ลักษณะเทียบกับส่วนต่อประสาน


345

ฉันพยายามศึกษาเกี่ยวกับ PHP เมื่อเร็ว ๆ นี้และฉันพบว่าตัวเองติดนิสัยแล้ว ฉันเข้าใจแนวคิดของการนำโค้ดแนวนอนมาใช้ใหม่และไม่ต้องการรับมรดกจากคลาสนามธรรม สิ่งที่ฉันไม่เข้าใจคือ: อะไรคือความแตกต่างที่สำคัญระหว่างการใช้คุณลักษณะกับส่วนต่อประสาน

ฉันได้ลองค้นหาโพสต์บล็อกที่ดีหรือบทความที่อธิบายว่าควรใช้อย่างใดอย่างหนึ่ง แต่ตัวอย่างที่ฉันพบจนถึงตอนนี้ดูเหมือนจะคล้ายกันมาก


6
interface ไม่มีรหัสใด ๆ ในส่วนของฟังก์ชั่น จริง ๆ แล้วพวกเขาไม่มีหน่วยงานใด ๆ
hakre

2
แม้จะมีคำตอบที่โหวตแล้วของฉัน แต่ฉันต้องการมันสำหรับบันทึกที่ฉันมักจะต่อต้านลักษณะ / มิกซ์อิน ตรวจสอบหลักฐานการแชทในการอ่านวิธีการลักษณะมักจะบ่อนทำลายการปฏิบัติ OOP ของแข็ง
rdlowrey

2
ฉันจะโต้แย้งในทางตรงกันข้าม ก่อนหน้านี้ฉันเคยทำงานกับ PHP มาหลายปีแล้วและนับตั้งแต่มีลักษณะมาฉันคิดว่ามันง่ายที่จะพิสูจน์คุณค่า เพียงอ่านตัวอย่างที่ใช้งานได้จริงซึ่งช่วยให้ 'แบบจำลองภาพ' สามารถเดินและพูดคุยเหมือนImagickวัตถุได้โดยไม่ต้องมีการขยายทั้งหมดที่จำเป็นในสมัยก่อนก่อนหน้านี้
quickshiftin

ลักษณะและส่วนต่อประสานนั้นคล้ายกัน ความแตกต่างที่สำคัญคือลักษณะช่วยให้คุณสามารถใช้วิธีการอินเทอร์เฟซไม่
จอห์น

คำตอบ:


239

อินเตอร์เฟสกำหนดชุดของเมธอดที่คลาสการนำไปใช้ต้องนำไปใช้

เมื่อเป็นลักษณะที่มีuse'd การใช้งานของวิธีการที่มาพร้อมเกินไป - Interfaceซึ่งไม่ได้เกิดขึ้นใน

นั่นคือความแตกต่างที่ยิ่งใหญ่ที่สุด

จากHorizontal Reuse สำหรับ PHP RFC :

ลักษณะเป็นกลไกสำหรับการใช้รหัสซ้ำในภาษาที่สืบทอดเดียวเช่น PHP ลักษณะนิสัยมีจุดประสงค์เพื่อลดข้อ จำกัด บางประการของการสืบทอดเดี่ยวโดยทำให้นักพัฒนาสามารถนำชุดวิธีการมาใช้ใหม่ได้อย่างอิสระในชั้นเรียนอิสระหลายแห่งที่อาศัยอยู่ในลำดับชั้นของชั้นเรียนที่แตกต่างกัน


2
@JREAM ในทางปฏิบัติไม่มีอะไร ในความเป็นจริงมากขึ้น
Alec Gorge

79
ยกเว้นว่าคุณลักษณะนั้นไม่ใช่อินเตอร์เฟสเลย อินเตอร์เฟสเป็นข้อกำหนดที่สามารถตรวจสอบได้ ไม่สามารถตรวจสอบลักษณะได้ดังนั้นจึงเป็นการนำไปใช้งานเท่านั้น ตรงข้ามกับอินเตอร์เฟส บรรทัดนั้นใน RFC นั้นผิดปกติ ...
ircmaxell

196
ลักษณะเป็นหลักภาษาช่วยคัดลอกและวาง
Shahid

10
นั่นไม่ใช่คำอุปมา นั่นคือการฆ่าความหมายของคำ มันเหมือนกับการอธิบายกล่องเป็นพื้นผิวที่มีปริมาตร
cleong

6
หากต้องการขยายความคิดเห็นของ ircmaxell และ Shadi: คุณสามารถตรวจสอบว่าวัตถุใช้อินเทอร์เฟซ (ผ่านอินสแตนซ์ของ) และคุณสามารถตรวจสอบให้แน่ใจว่าอาร์กิวเมนต์เมธอดใช้อินเทอร์เฟซผ่านคำใบ้ประเภทในลายเซ็นวิธีการ คุณไม่สามารถตรวจสอบคุณสมบัติที่เกี่ยวข้องได้
Brian D'Astous

531

ประกาศบริการสาธารณะ:

ฉันต้องการระบุสำหรับบันทึกที่ฉันเชื่อว่าลักษณะเป็นกลิ่นรหัสเกือบทุกครั้งและควรหลีกเลี่ยงในความโปรดปรานขององค์ประกอบ เป็นความเห็นของฉันว่าการสืบทอดเพียงครั้งเดียวมักถูกทารุณกรรมจนถึงจุดที่เป็นรูปแบบการต่อต้านและการสืบทอดหลายอย่างเท่านั้นที่รวมปัญหานี้ คุณจะได้รับบริการที่ดีขึ้นในหลาย ๆ กรณีโดยการจัดองค์ประกอบการสืบทอด (ไม่ว่าจะเป็นรายการเดียวหรือหลายรายการ) หากคุณยังคงสนใจในลักษณะและความสัมพันธ์ของพวกเขากับอินเทอร์เฟซอ่านต่อ ...


เริ่มต้นด้วยการพูดสิ่งนี้:

การเขียนโปรแกรมเชิงวัตถุ (OOP) อาจเป็นกระบวนทัศน์ที่ยากต่อการเข้าใจ เพียงเพราะคุณกำลังใช้คลาสไม่ได้หมายความว่ารหัสของคุณคือ Object-Oriented (OO)

ในการเขียนรหัส OO คุณต้องเข้าใจว่า OOP นั้นเกี่ยวกับความสามารถของวัตถุของคุณจริงๆ คุณต้องคิดถึงคลาสในแง่ของสิ่งที่พวกเขาสามารถทำได้แทนที่จะเป็นสิ่งที่พวกเขาทำจริงๆ สิ่งนี้ตรงกันข้ามกับการเขียนโปรแกรมตามขั้นตอนแบบดั้งเดิมโดยมุ่งเน้นที่การสร้างโค้ด "ทำบางสิ่งบางอย่าง"

ถ้ารหัส OOP เกี่ยวกับการวางแผนและออกแบบส่วนต่อประสานเป็นพิมพ์เขียวและวัตถุเป็นบ้านที่สร้างขึ้นอย่างสมบูรณ์ ในขณะเดียวกันลักษณะเป็นเพียงวิธีที่จะช่วยสร้างบ้านที่วางโดยพิมพ์เขียว (อินเทอร์เฟซ)

อินเตอร์เฟซ

ดังนั้นทำไมเราจึงควรใช้อินเตอร์เฟส ค่อนข้างง่ายอินเตอร์เฟสทำให้โค้ดของเราเปราะน้อยลง หากคุณสงสัยในข้อความนี้ให้ถามผู้ที่ถูกบังคับให้รักษารหัสดั้งเดิมที่ไม่ได้เขียนไว้กับส่วนต่อประสาน

อินเทอร์เฟซเป็นสัญญาระหว่างโปรแกรมเมอร์และรหัสของเขา / เธอ อินเทอร์เฟซกล่าวว่า "ตราบใดที่คุณเล่นตามกฎของฉันคุณสามารถใช้ฉันได้ตามที่คุณต้องการและฉันสัญญาว่าฉันจะไม่ทำลายรหัสอื่นของคุณ"

เพื่อเป็นตัวอย่างให้พิจารณาสถานการณ์จริง (ไม่มีรถยนต์หรือวิดเจ็ต):

คุณต้องการใช้ระบบแคชสำหรับเว็บแอปพลิเคชันเพื่อลดภาระของเซิร์ฟเวอร์

คุณเริ่มต้นด้วยการเขียนชั้นเรียนเพื่อแคตอบคำขอโดยใช้ APC:

class ApcCacher
{
  public function fetch($key) {
    return apc_fetch($key);
  }
  public function store($key, $data) {
    return apc_store($key, $data);
  }
  public function delete($key) {
    return apc_delete($key);
  }
}

จากนั้นในวัตถุตอบกลับ HTTP ของคุณคุณตรวจสอบแคชการเข้าชมก่อนที่จะทำงานทั้งหมดเพื่อสร้างการตอบสนองที่แท้จริง:

class Controller
{
  protected $req;
  protected $resp;
  protected $cacher;

  public function __construct(Request $req, Response $resp, ApcCacher $cacher=NULL) {
    $this->req    = $req;
    $this->resp   = $resp;
    $this->cacher = $cacher;

    $this->buildResponse();
  }

  public function buildResponse() {
    if (NULL !== $this->cacher && $response = $this->cacher->fetch($this->req->uri()) {
      $this->resp = $response;
    } else {
      // Build the response manually
    }
  }

  public function getResponse() {
    return $this->resp;
  }
}

วิธีนี้ใช้งานได้ดี แต่ไม่กี่สัปดาห์ต่อมาคุณตัดสินใจว่าคุณต้องการใช้ระบบแคชแบบไฟล์แทน APC ตอนนี้คุณต้องเปลี่ยนรหัสคอนโทรลเลอร์ของคุณเพราะคุณได้ตั้งโปรแกรมคอนโทรลเลอร์ให้ทำงานกับฟังก์ชันการทำงานของApcCacherคลาสแทนที่จะเป็นอินเทอร์เฟซที่แสดงความสามารถของApcCacherคลาส สมมติว่าแทนที่จะเป็นข้างต้นคุณได้ให้Controllerชั้นเรียนพึ่งพาCacherInterfaceแทนที่จะเป็นรูปธรรมApcCacherดังนี้:

// Your controller's constructor using the interface as a dependency
public function __construct(Request $req, Response $resp, CacherInterface $cacher=NULL)

หากต้องการไปพร้อมกับที่คุณกำหนดอินเทอร์เฟซของคุณเช่น:

interface CacherInterface
{
  public function fetch($key);
  public function store($key, $data);
  public function delete($key);
}

ในทางกลับกันคุณจะมีทั้งคลาสของคุณApcCacherและFileCacherคลาสใหม่ของคุณใช้CacherInterfaceและคุณตั้งโปรแกรมControllerคลาสของคุณเพื่อใช้ความสามารถที่ต้องการโดยอินเทอร์เฟซ

ตัวอย่างนี้ (หวังว่า) แสดงให้เห็นว่าการเขียนโปรแกรมไปยังอินเทอร์เฟซช่วยให้คุณเปลี่ยนการใช้งานภายในห้องเรียนของคุณได้อย่างไรโดยไม่ต้องกังวลว่าการเปลี่ยนแปลงจะทำให้รหัสอื่นของคุณเสียหาย

ลักษณะ

ในทางกลับกันคุณสมบัติเป็นเพียงวิธีการใช้รหัสอีกครั้ง การเชื่อมต่อไม่ควรถูกมองว่าเป็นทางเลือกที่ไม่เหมือนกัน ในความเป็นจริงการสร้างลักษณะที่ตอบสนองความสามารถที่จำเป็นโดยอินเตอร์เฟซเป็นกรณีการใช้งานที่เหมาะ

คุณควรใช้ลักษณะเฉพาะเมื่อมีหลายคลาสที่ใช้ฟังก์ชันเดียวกันร่วมกัน (อาจเป็นไปได้โดยอินเทอร์เฟซเดียวกัน) ไม่มีความหมายในการใช้คุณลักษณะเพื่อให้มีฟังก์ชั่นสำหรับคลาสเดียวนั่นคือทำให้งงงวยว่าสิ่งใดที่คลาสทำและการออกแบบที่ดีกว่าจะย้ายการทำงานของคุณลักษณะไปยังคลาสที่เกี่ยวข้อง

พิจารณาการใช้คุณลักษณะต่อไปนี้:

interface Person
{
    public function greet();
    public function eat($food);
}

trait EatingTrait
{
    public function eat($food)
    {
        $this->putInMouth($food);
    }

    private function putInMouth($food)
    {
        // Digest delicious food
    }
}

class NicePerson implements Person
{
    use EatingTrait;

    public function greet()
    {
        echo 'Good day, good sir!';
    }
}

class MeanPerson implements Person
{
    use EatingTrait;

    public function greet()
    {
        echo 'Your mother was a hamster!';
    }
}

ตัวอย่างที่เป็นรูปธรรมมากขึ้น: ลองจินตนาการว่าทั้งคุณFileCacherและของคุณApcCacherจากการพูดคุยในส่วนต่อประสานใช้วิธีการเดียวกันเพื่อกำหนดว่ารายการแคชเก่าและควรถูกลบหรือไม่ (เห็นได้ชัดว่านี่ไม่ใช่กรณีในชีวิตจริง คุณสามารถเขียนคุณลักษณะและอนุญาตให้ทั้งสองคลาสใช้สำหรับข้อกำหนดอินเทอร์เฟซทั่วไป

หนึ่งคำสุดท้ายที่ควรระวัง: ระวังอย่าไปลงเรือด้วยคุณลักษณะ บ่อยครั้งที่มีการใช้ลักษณะเป็นไม้ยันรักแร้สำหรับการออกแบบที่ไม่ดีเมื่อการใช้งานระดับที่ไม่ซ้ำกันจะเพียงพอ คุณควร จำกัด คุณสมบัติในการปฏิบัติตามข้อกำหนดของอินเทอร์เฟซสำหรับการออกแบบรหัสที่ดีที่สุด


69
ฉันกำลังมองหาคำตอบง่ายๆที่ให้ไว้ข้างต้น แต่ฉันต้องบอกว่าคุณให้คำตอบในเชิงลึกที่ยอดเยี่ยมซึ่งจะช่วยให้ความแตกต่างชัดเจนขึ้นสำหรับคนอื่น ๆ รุ่งโรจน์
datguywhowanders

35
"[C] ลักษณะการเลี้ยงที่เติมเต็มความสามารถที่ต้องการโดยอินเทอร์เฟซในคลาสที่กำหนดเป็นกรณีการใช้ในอุดมคติ" เผง: +1
Alec Gorge

5
มันจะยุติธรรมที่จะบอกว่าลักษณะใน PHP คล้ายกับ mixins ในภาษาอื่น ๆ ?
Eno

5
@igorpan สำหรับทุกเจตนารมณ์และวัตถุประสงค์ฉันจะบอกว่าการใช้คุณลักษณะของ PHP นั้นเหมือนกับมรดกหลายอย่าง เป็นที่น่าสังเกตว่าถ้าคุณสมบัติใน PHP ระบุคุณสมบัติสแตติกแต่ละคลาสที่ใช้คุณสมบัติจะมีสำเนาของคุณสมบัติสแตติกของตัวเอง ที่สำคัญยิ่งกว่า ... การเห็นว่าโพสต์นี้สูงมากใน SERPs อย่างไรเมื่อทำการสอบถามคุณสมบัติฉันจะเพิ่มประกาศการบริการสาธารณะที่ด้านบนของหน้า คุณควรอ่านมัน
rdlowrey

3
+1 สำหรับคำอธิบายในเชิงลึก ฉันมาจากพื้นหลังทับทิมที่มีการใช้ mixins มาก; เพียงเพื่อเพิ่มสองเซ็นต์ของฉันกฎง่ายๆที่เราใช้นั้นสามารถแปลเป็น PHP ได้ว่า "อย่าใช้วิธีการที่เปลี่ยนแปลง $ this ในลักษณะ" สิ่งนี้จะป้องกันเซสชันการดีบักแบบบ้าคลั่งทั้งหมด ... มิกซ์อินไม่ควรตั้งสมมติฐานใด ๆ ในคลาสที่จะนำมาผสมกัน (หรือคุณควรทำให้ชัดเจนและลดการพึ่งพาให้เหลือน้อยที่สุด) ในเรื่องนี้ฉันพบว่าความคิดของคุณเกี่ยวกับลักษณะการใช้อินเตอร์เฟสที่ดี
m_x

67

traitเป็นหลักการดำเนินงานของ PHP ของmixinและมีประสิทธิภาพชุดของวิธีการขยายที่สามารถเพิ่มการเรียนใด ๆ traitผ่านทางนอกเหนือจากที่ วิธีการนั้นจะกลายเป็นส่วนหนึ่งของการดำเนินงานระดับนั้น แต่โดยไม่ต้องใช้การถ่ายทอดทางพันธุกรรม

จากคู่มือ PHP (เหมืองที่เน้น):

ลักษณะเป็นกลไกสำหรับการใช้รหัสซ้ำในภาษาที่สืบทอดเดียวเช่น PHP ... มันเป็นการเพิ่มมรดกดั้งเดิมและช่วยให้สามารถจัดองค์ประกอบพฤติกรรมในแนวนอนได้ นั่นคือการประยุกต์ใช้สมาชิกชั้นเรียนโดยไม่ต้องรับมรดก

ตัวอย่าง:

trait myTrait {
    function foo() { return "Foo!"; }
    function bar() { return "Bar!"; }
}

ด้วยคุณสมบัติที่กำหนดข้างต้นฉันสามารถทำสิ่งต่อไปนี้ได้:

class MyClass extends SomeBaseClass {
    use myTrait; // Inclusion of the trait myTrait
}

ณ จุดนี้เมื่อฉันสร้างตัวอย่างของคลาสMyClassก็มีสองวิธีที่เรียกว่าfoo()และbar()- myTraitซึ่งมาจาก และ - โปรดสังเกตว่าtraitวิธี -defined มีเนื้อหาของเมธอดอยู่แล้ว - ซึ่งInterfaceเมธอด -defined ไม่สามารถทำได้

นอกจากนี้ - PHP เช่นเดียวกับภาษาอื่น ๆ อีกมากมายใช้รูปแบบการสืบทอดเดียวซึ่งหมายความว่าคลาสสามารถสืบทอดมาจากหลายอินเตอร์เฟส แต่ไม่ใช่คลาสหลายคลาส อย่างไรก็ตามคลาส PHP สามารถมีtraitการรวมหลายอย่าง- ซึ่งทำให้โปรแกรมเมอร์สามารถรวมชิ้นส่วนที่นำกลับมาใช้ใหม่ได้เช่นเดียวกับที่อาจรวมถึงคลาสพื้นฐานหลายคลาส

สิ่งที่ควรทราบ:

                      -----------------------------------------------
                      |   Interface   |  Base Class   |    Trait    |
                      ===============================================
> 1 per class         |      Yes      |       No      |     Yes     |
---------------------------------------------------------------------
Define Method Body    |      No       |       Yes     |     Yes     |
---------------------------------------------------------------------
Polymorphism          |      Yes      |       Yes     |     No      |
---------------------------------------------------------------------

polymorphism:

ในตัวอย่างก่อนหน้านี้ที่MyClass ขยาย SomeBaseClass , MyClass เป็นSomeBaseClassตัวอย่างของ ในคำอื่น ๆ อาร์เรย์เช่นสามารถมีกรณีของSomeBaseClass[] bases MyClassในทำนองเดียวกันถ้าMyClassขยายIBaseInterfaceอาร์เรย์ของอาจมีกรณีของIBaseInterface[] bases MyClassไม่มีโครงสร้าง polymorphic ดังกล่าวที่ใช้ได้กับtrait- เพราะ a traitเป็นเพียงรหัสซึ่งถูกคัดลอกเพื่อความสะดวกของโปรแกรมเมอร์ในแต่ละชั้นเรียนที่ใช้มัน

ความสำคัญ:

ตามที่อธิบายไว้ในคู่มือ:

สมาชิกที่สืบทอดจากคลาสพื้นฐานถูกแทนที่โดยสมาชิกที่แทรกโดยลักษณะ ลำดับความสำคัญคือสมาชิกจากวิธีการแทนที่ชั้นเรียนปัจจุบันซึ่งในวิธีการแทนที่กลับมารับมรดก

ดังนั้น - พิจารณาสถานการณ์สมมติต่อไปนี้:

class BaseClass {
    function SomeMethod() { /* Do stuff here */ }
}

interface IBase {
    function SomeMethod();
}

trait myTrait {
    function SomeMethod() { /* Do different stuff here */ }
}

class MyClass extends BaseClass implements IBase {
    use myTrait;

    function SomeMethod() { /* Do a third thing */ }
}

เมื่อสร้างอินสแตนซ์ของ MyClass ด้านบนจะเกิดสิ่งต่อไปนี้:

  1. Interface IBaseต้องใช้ฟังก์ชั่นที่เรียกว่า parameterless SomeMethod()ที่จะให้
  2. ชั้นฐานBaseClassให้การดำเนินการตามวิธีนี้ - ตอบสนองความต้องการ
  3. trait myTraitให้ฟังก์ชันการ parameterless ที่เรียกว่าSomeMethod()เป็นอย่างดีซึ่งจะมีความสำคัญมากกว่าBaseClassเวอร์ชั่น
  4. class MyClassให้รุ่นของตนเองSomeMethod()- ซึ่งจะมีความสำคัญมากกว่าtraitเวอร์ชั่น

ข้อสรุป

  1. Interfaceไม่สามารถให้การดำเนินงานที่เริ่มต้นของร่างกายวิธีการในขณะที่traitสามารถ
  2. Interfaceเป็นpolymorphic , สืบทอดมาสร้าง - ในขณะที่traitไม่ได้เป็น
  3. หลายInterfaces สามารถใช้ในชั้นเรียนเดียวกันและสามารถหลายtraits

4
"ลักษณะคล้ายกับแนวคิด C # ของคลาสนามธรรม" ไม่คลาสนามธรรมเป็นคลาสนามธรรม แนวคิดนั้นมีอยู่ทั้งใน PHP และ C # ฉันจะเปรียบเทียบลักษณะใน PHP กับคลาสแบบสแตติกที่ทำจากวิธีการขยายใน C # แทนโดยมีข้อ จำกัด ตามประเภทออกเนื่องจากลักษณะสามารถใช้งานได้ทุกประเภทไม่เหมือนกับวิธีการขยายที่ขยายเพียงหนึ่งประเภทเท่านั้น
BoltClock

1
ความเห็นที่ดีมาก - และฉันเห็นด้วยกับคุณ ในการอ่านซ้ำนั่นเป็นการเปรียบเทียบที่ดีกว่า ฉันเชื่อว่ามันยังดีกว่าถ้าคิดว่ามันเป็นmixin- และเมื่อฉันกลับมาเปิดคำตอบของฉันฉันได้รับการปรับปรุงเพื่อสะท้อนสิ่งนี้ ขอบคุณสำหรับการแสดงความคิดเห็น @BoltClock!
ทรอย Alford

1
ฉันไม่คิดว่ามีความสัมพันธ์กับวิธีการขยาย c # ใด ๆ มีการเพิ่มวิธีการขยายไปยังประเภทชั้นเดียว (ตามลำดับชั้นของหลักสูตร) ​​จุดประสงค์ของพวกเขาคือเพื่อปรับปรุงประเภทที่มีฟังก์ชั่นเพิ่มเติมไม่ให้ "แบ่งปันรหัส" เหนือชั้นเรียนหลายชั้นและทำให้เป็นระเบียบ มันไม่ได้เปรียบเทียบกัน! หากจำเป็นต้องนำสิ่งกลับมาใช้ใหม่ก็มักจะหมายความว่าควรมีพื้นที่ของตัวเองเช่นชั้นแยกซึ่งจะเกี่ยวข้องกับชั้นเรียนที่ต้องการฟังก์ชั่นทั่วไป การใช้งานอาจแตกต่างกันไปขึ้นอยู่กับการออกแบบ แต่โดยประมาณนั้น ลักษณะเป็นอีกวิธีหนึ่งในการสร้างรหัสที่ไม่ดี
Sofija

คลาสสามารถมีหลายอินเตอร์เฟสได้หรือไม่ ฉันไม่แน่ใจว่าฉันทำกราฟของคุณผิดหรือไม่ แต่คลาส X ใช้ Y, Z นั้นถูกต้อง
Yann Chabot

26

ฉันคิดว่าtraitsมีประโยชน์ในการสร้างชั้นเรียนที่มีวิธีการที่สามารถใช้เป็นวิธีการเรียนที่แตกต่างกันหลาย

ตัวอย่างเช่น:

trait ToolKit
{
    public $errors = array();

    public function error($msg)
    {
        $this->errors[] = $msg;
        return false;
    }
}

คุณสามารถมีและใช้ "ข้อผิดพลาด" วิธีนี้ในชั้นเรียนใด ๆ ที่ใช้ลักษณะนี้

class Something
{
    use Toolkit;

    public function do_something($zipcode)
    {
        if (preg_match('/^[0-9]{5}$/', $zipcode) !== 1)
            return $this->error('Invalid zipcode.');

        // do something here
    }
}

ในขณะที่อยู่กับinterfacesคุณเท่านั้นที่สามารถประกาศลายเซ็นวิธีการ แต่ไม่รหัสฟังก์ชั่น implementsนอกจากนี้การใช้อินเตอร์เฟซที่คุณจำเป็นต้องทำตามลำดับชั้นโดยใช้ นี่ไม่ใช่กรณีที่มีลักษณะ

มันแตกต่างอย่างสิ้นเชิง!


ฉันคิดว่านี่เป็นตัวอย่างที่ไม่ดีของลักษณะนิสัย to_integerจะมีโอกาสมากขึ้นที่จะรวมอยู่ในIntegerCastอินเตอร์เฟสเนื่องจากไม่มีวิธีที่คล้ายกันในการส่งคลาสไปยังจำนวนเต็ม (อย่างชาญฉลาด)
แมทธิว

5
ลืม "to_integer" - เป็นเพียงภาพประกอบ ตัวอย่าง. "สวัสดีโลก" "example.com"
J. Bruni

2
ลักษณะพิเศษของชุดเครื่องมือนี้มีประโยชน์อะไรบ้างที่คลาสยูทิลิตี้แบบสแตนด์อโลนไม่สามารถทำได้ แทนที่จะเป็นไปuse Toolkitได้$this->toolkit = new Toolkit();หรือว่าฉันขาดคุณสมบัติบางอย่างไปเอง?
Anthony

@ อันธพาลอยู่ที่ไหนสักแห่งในSomethingตู้คอนเทนเนอร์ของคุณif(!$something->do_something('foo')) var_dump($something->errors);
TheRealChx101

20

สำหรับผู้เริ่มต้นคำตอบข้างต้นอาจเป็นเรื่องยากนี่เป็นวิธีที่ง่ายที่สุดในการเข้าใจ:

ลักษณะ

trait SayWorld {
    public function sayHello() {
        echo 'World!';
    }
}

ดังนั้นหากคุณต้องการมีsayHelloฟังก์ชั่นในคลาสอื่นโดยไม่ต้องสร้างฟังก์ชั่นใหม่ทั้งหมดคุณสามารถใช้คุณลักษณะ

class MyClass{
  use SayWorld;

}

$o = new MyClass();
$o->sayHello();

เยี่ยมเลย!

ฟังก์ชั่นไม่เพียง แต่คุณสามารถใช้สิ่งใดในลักษณะ (ฟังก์ชั่น, ตัวแปร, const .. ) นอกจากนี้คุณสามารถใช้หลายลักษณะ:use SayWorld,AnotherTraits;

อินเตอร์เฟซ

  interface SayWorld {
     public function sayHello();
  }

  class MyClass implements SayWorld { 
     public function sayHello() {
        echo 'World!';
     }
}

ดังนั้นนี่คือความแตกต่างของอินเทอร์เฟซ: คุณต้องสร้างทุกสิ่งในอินเทอร์เฟซในคลาสที่นำไปใช้ใหม่ อินเทอร์เฟซไม่มีการใช้งาน และอินเตอร์เฟสสามารถมีฟังก์ชั่นและ const เท่านั้นมันไม่สามารถมีตัวแปรได้

ฉันหวังว่านี่จะช่วยได้!


5

อุปมาที่ใช้บ่อยเพื่ออธิบายลักษณะเป็นลักษณะเป็นส่วนต่อประสานกับการใช้งาน

นี่เป็นวิธีที่ดีในการคิดเกี่ยวกับมันในสถานการณ์ส่วนใหญ่ แต่มีความแตกต่างเล็กน้อยระหว่างทั้งสอง

สำหรับการเริ่มต้นinstanceofโอเปอเรเตอร์จะไม่ทำงานกับคุณลักษณะ (เช่นลักษณะไม่ใช่วัตถุจริง) ดังนั้นคุณไม่สามารถที่เราจะดูว่าชั้นมีลักษณะบางอย่าง (หรือเพื่อดูว่าสองชั้นที่ไม่เกี่ยวข้องกันแบ่งปันลักษณะ ) นั่นคือสิ่งที่พวกเขาหมายถึงว่ามันเป็นโครงสร้างสำหรับการนำโค้ดแนวนอนกลับมาใช้ใหม่

มีมีฟังก์ชั่นในขณะนี้ใน PHP ที่จะช่วยให้คุณได้รับรายชื่อของทุกลักษณะที่ใช้ระดับลักษณะมรดกหมายความว่าคุณจะต้องทำการตรวจสอบ recursive เชื่อถือได้ตรวจสอบว่าระดับในบางจุดที่มีลักษณะเฉพาะเจาะจง แต่ (มีตัวอย่างเช่น รหัสในหน้า PHP doco) แต่ใช่แน่นอนไม่ง่ายและสะอาดอย่างที่เป็นอยู่และ IMHO เป็นคุณลักษณะที่จะทำให้ PHP ดีขึ้น

นอกจากนี้คลาสนามธรรมยังคงเป็นคลาสดังนั้นจึงไม่สามารถแก้ปัญหาการใช้ซ้ำของรหัสที่เกี่ยวข้องกับมรดกได้ จำไว้ว่าคุณสามารถขยายได้เพียงหนึ่งคลาส (จริงหรือนามธรรม) แต่ใช้หลายอินเตอร์เฟส

ฉันพบว่าคุณลักษณะและอินเทอร์เฟซดีมากที่ใช้มือจับคู่กันเพื่อสร้างการสืบทอดหลายหลอก เช่น:

class SlidingDoor extends Door implements IKeyed  
{  
    use KeyedTrait;  
    [...] // Generally not a lot else goes here since it's all in the trait  
}

การทำเช่นนี้หมายความว่าคุณสามารถใช้อินสแตนซ์ของการตรวจสอบว่าวัตถุประตูเฉพาะคีย์หรือไม่คุณรู้ว่าคุณจะได้รับชุดวิธีการที่สอดคล้องกัน ฯลฯ และรหัสทั้งหมดอยู่ในที่เดียวในทุกชั้นเรียนที่ใช้ KeyedTrait


ส่วนสุดท้ายของคำตอบนั้นแน่นอนว่าสิ่งที่ @lowlowrey พูดถึงรายละเอียดเพิ่มเติมในสามย่อหน้าสุดท้ายภายใต้ "ลักษณะ" ในโพสต์ของเขา ฉันเพิ่งรู้สึกว่าข้อมูลโค้ดโครงกระดูกง่าย ๆ จริง ๆ จะช่วยอธิบายได้
Jon Kloske

ฉันคิดว่าวิธีที่ดีที่สุดในการใช้คุณลักษณะ OO คือการใช้ส่วนต่อประสานที่คุณสามารถทำได้ และหากมีกรณีเมื่อหลายคลาสย่อยที่ใช้รหัสชนิดเดียวกันสำหรับอินเทอร์เฟซนั้นและคุณไม่สามารถย้ายรหัสนั้นไปที่ซูเปอร์คลาส (นามธรรม) -> นำไปใช้กับคุณลักษณะ
player-one

4

ลักษณะเป็นเพียงสำหรับนำมาใช้รหัส

อินเตอร์เฟซที่เพียงแค่มีลายเซ็นของฟังก์ชั่นที่จะถูกกำหนดไว้ในชั้นเรียนที่จะสามารถนำมาใช้ขึ้นอยู่กับดุลยพินิจของโปรแกรมเมอร์ ดังนั้นให้เราต้นแบบสำหรับกลุ่มชั้นเรียน

สำหรับการอ้างอิง - http://www.php.net/manual/en/language.oop5.traits.php


3

คุณสามารถพิจารณาลักษณะเป็นโค้ด "copy-paste" โดยอัตโนมัติ

การใช้ลักษณะเป็นสิ่งที่อันตรายเนื่องจากไม่มีความหมายที่จะรู้ว่ามันทำอะไรก่อนการประหารชีวิต

อย่างไรก็ตามคุณลักษณะมีความยืดหยุ่นมากกว่าเนื่องจากไม่มีข้อ จำกัด เช่นการสืบทอด

ลักษณะจะมีประโยชน์ในการฉีดวิธีที่ตรวจสอบบางสิ่งบางอย่างในชั้นเรียนตัวอย่างเช่นการดำรงอยู่ของวิธีการหรือคุณลักษณะอื่น บทความที่ดีในที่ ( แต่ในภาษาฝรั่งเศสขออภัย)

สำหรับผู้อ่านภาษาฝรั่งเศสที่สามารถอ่านได้นิตยสาร GNU / Linux HS 54 มีบทความเกี่ยวกับเรื่องนี้


ยังไม่ทราบว่าคุณลักษณะแตกต่างจากอินเทอร์เฟซที่มีการใช้งานเริ่มต้นอย่างไร
denis631

@ denis631 คุณสามารถเห็นลักษณะเป็นตัวอย่างของรหัสและอินเทอร์เฟซเป็นสัญญาลายเซ็น ถ้ามันสามารถช่วยได้คุณจะเห็นว่ามันเป็นบิตที่ไม่เป็นทางการของคลาสที่สามารถมีอะไรก็ได้ แจ้งให้เราทราบหากช่วยได้
Benj

ฉันเห็นว่าลักษณะ PHP สามารถมองเห็นได้เป็นมาโครซึ่งจะถูกขยายในเวลาคอมไพล์ / เพียงแค่สร้างนามแฝงโค้ดนั้นด้วยคีย์นั้น อย่างไรก็ตามลักษณะสนิมปรากฏแตกต่างกัน (หรือฉันผิด) แต่เนื่องจากพวกเขาทั้งสองมีคุณลักษณะของคำฉันคิดว่าพวกเขาจะเหมือนกันจึงหมายถึงแนวคิดเดียวกัน ลิงก์ลักษณะสนิม: doc.rust-lang.org/rust-by-example/trait.html
denis631

2

ถ้าคุณรู้ภาษาอังกฤษและรู้traitว่ามันแปลว่าอะไร useมันเป็นแพ็คระดับน้อยของวิธีการและคุณสมบัติที่คุณแนบไปกับการเรียนที่มีอยู่โดยการพิมพ์

โดยทั่วไปคุณสามารถเปรียบเทียบกับตัวแปรเดียว ฟังก์ชั่นการปิดสามารถuseตัวแปรเหล่านี้จากนอกขอบเขตและวิธีการที่พวกเขามีค่าอยู่ภายใน พวกมันมีพลังและสามารถใช้ได้ในทุกสิ่ง เกิดขึ้นกับลักษณะเช่นกันหากมีการใช้งาน


2

คำตอบอื่น ๆ เป็นงานที่ยอดเยี่ยมในการอธิบายความแตกต่างระหว่างอินเตอร์เฟสและคุณลักษณะ ฉันจะมุ่งเน้นไปที่ตัวอย่างที่มีประโยชน์ในโลกแห่งความเป็นจริงโดยเฉพาะอย่างยิ่งซึ่งแสดงให้เห็นว่าลักษณะสามารถใช้ตัวแปรอินสแตนซ์ - ช่วยให้คุณเพิ่มพฤติกรรมให้กับชั้นเรียนที่มีรหัสสำเร็จรูปน้อยที่สุด

อีกครั้งเช่นที่ผู้อื่นกล่าวถึงลักษณะที่จับคู่ได้ดีกับส่วนต่อประสานช่วยให้อินเทอร์เฟซเพื่อระบุสัญญาพฤติกรรมและลักษณะการทำงานเพื่อให้บรรลุการใช้งาน

การเพิ่มความสามารถในการเผยแพร่ / สมัครสมาชิกของเหตุการณ์ไปยังคลาสสามารถเป็นสถานการณ์จำลองทั่วไปในบางรหัสฐาน มี 3 โซลูชันทั่วไป:

  1. กำหนดคลาสพื้นฐานด้วย event pub / sub code จากนั้นคลาสที่ต้องการเสนอเหตุการณ์สามารถขยายได้เพื่อเพิ่มความสามารถ
  2. กำหนดคลาสด้วย event pub / sub code จากนั้นคลาสอื่น ๆ ที่ต้องการเสนอเหตุการณ์สามารถใช้งานได้ผ่านการจัดองค์ประกอบการกำหนดวิธีการของตนเองเพื่อล้อมวัตถุที่ประกอบไว้
  3. กำหนดลักษณะด้วย event pub / sub code จากนั้นคลาสอื่น ๆ ที่ต้องการเสนอเหตุการณ์สามารถuseนำคุณลักษณะหรือที่รู้จักในการนำเข้ามาเพื่อเพิ่มขีดความสามารถ

แต่ละงานทำได้ดีแค่ไหน?

# 1 ทำงานได้ไม่ดี จนกว่าจะถึงวันที่คุณรู้ว่าคุณไม่สามารถขยายชั้นฐานได้เพราะคุณกำลังขยายสิ่งอื่นอยู่แล้ว ฉันจะไม่แสดงตัวอย่างของสิ่งนี้เพราะมันควรจะชัดเจนว่าการ จำกัด การใช้มรดกเช่นนี้

# 2 & # 3 ทำงานได้ดี ฉันจะแสดงตัวอย่างที่เน้นความแตกต่างบางอย่าง

ขั้นแรกให้โค้ดบางส่วนที่จะเหมือนกันระหว่างตัวอย่างทั้งสอง:

อินเทอร์เฟซ

interface Observable {
    function addEventListener($eventName, callable $listener);
    function removeEventListener($eventName, callable $listener);
    function removeAllEventListeners($eventName);
}

และรหัสบางอย่างเพื่อแสดงการใช้งาน:

$auction = new Auction();

// Add a listener, so we know when we get a bid.
$auction->addEventListener('bid', function($bidderName, $bidAmount){
    echo "Got a bid of $bidAmount from $bidderName\n";
});

// Mock some bids.
foreach (['Moe', 'Curly', 'Larry'] as $name) {
    $auction->addBid($name, rand());
}

ตกลงตอนนี้ให้แสดงวิธีการใช้งานของ Auctionชั้นจะแตกต่างกันเมื่อใช้ลักษณะ

ก่อนอื่นต่อไปนี้เป็นลักษณะที่ # 2 (การใช้การแต่งเพลง) ที่มีลักษณะดังนี้:

class EventEmitter {
    private $eventListenersByName = [];

    function addEventListener($eventName, callable $listener) {
        $this->eventListenersByName[$eventName][] = $listener;
    }

    function removeEventListener($eventName, callable $listener) {
        $this->eventListenersByName[$eventName] = array_filter($this->eventListenersByName[$eventName], function($existingListener) use ($listener) {
            return $existingListener === $listener;
        });
    }

    function removeAllEventListeners($eventName) {
        $this->eventListenersByName[$eventName] = [];
    }

    function triggerEvent($eventName, array $eventArgs) {
        foreach ($this->eventListenersByName[$eventName] as $listener) {
            call_user_func_array($listener, $eventArgs);
        }
    }
}

class Auction implements Observable {
    private $eventEmitter;

    public function __construct() {
        $this->eventEmitter = new EventEmitter();
    }

    function addBid($bidderName, $bidAmount) {
        $this->eventEmitter->triggerEvent('bid', [$bidderName, $bidAmount]);
    }

    function addEventListener($eventName, callable $listener) {
        $this->eventEmitter->addEventListener($eventName, $listener);
    }

    function removeEventListener($eventName, callable $listener) {
        $this->eventEmitter->removeEventListener($eventName, $listener);
    }

    function removeAllEventListeners($eventName) {
        $this->eventEmitter->removeAllEventListeners($eventName);
    }
}

นี่คือลักษณะ # 3 (ลักษณะ):

trait EventEmitterTrait {
    private $eventListenersByName = [];

    function addEventListener($eventName, callable $listener) {
        $this->eventListenersByName[$eventName][] = $listener;
    }

    function removeEventListener($eventName, callable $listener) {
        $this->eventListenersByName[$eventName] = array_filter($this->eventListenersByName[$eventName], function($existingListener) use ($listener) {
            return $existingListener === $listener;
        });
    }

    function removeAllEventListeners($eventName) {
        $this->eventListenersByName[$eventName] = [];
    }

    protected function triggerEvent($eventName, array $eventArgs) {
        foreach ($this->eventListenersByName[$eventName] as $listener) {
            call_user_func_array($listener, $eventArgs);
        }
    }
}

class Auction implements Observable {
    use EventEmitterTrait;

    function addBid($bidderName, $bidAmount) {
        $this->triggerEvent('bid', [$bidderName, $bidAmount]);
    }
}

โปรดทราบว่ารหัสภายในEventEmitterTraitนั้นเหมือนกับสิ่งที่อยู่ในEventEmitterชั้นเรียนยกเว้นลักษณะประกาศว่าtriggerEvent()วิธีการป้องกัน ดังนั้นความแตกต่างเพียงอย่างเดียวที่คุณต้องดูคือการนำAuctionคลาสไปใช้

และความแตกต่างมีขนาดใหญ่ เมื่อใช้การแต่งเพลงเราจะได้คำตอบที่ยอดเยี่ยมทำให้เราสามารถนำEventEmitterคลาสของเรากลับมาใช้ใหม่ได้ตามต้องการ แต่ข้อเสียเปรียบหลักคือเรามีรหัสสำเร็จรูปจำนวนมากที่เราจำเป็นต้องเขียนและบำรุงรักษาเพราะสำหรับแต่ละวิธีที่กำหนดไว้ในObservableส่วนต่อประสานเราจำเป็นต้องใช้มันและเขียนรหัสสำเร็จรูปที่น่าเบื่อที่ส่งต่อข้อโต้แย้งไปยังวิธีการEventEmitterวัตถุที่เราแต่งขึ้นนั้น โดยใช้ลักษณะในตัวอย่างนี้จะช่วยให้เราหลีกเลี่ยงที่ช่วยให้เราลดรหัสสำเร็จรูปและปรับปรุงการบำรุงรักษา

อย่างไรก็ตามอาจมีบางครั้งที่คุณไม่ต้องการให้Auctionคลาสของคุณใช้Observableอินเทอร์เฟซเต็มรูปแบบ- บางทีคุณอาจต้องการเปิดเผยวิธีการ 1 หรือ 2 หรืออาจไม่มีเลยก็ได้เพื่อให้คุณสามารถกำหนดลายเซ็นวิธีการของคุณเอง ในกรณีเช่นนี้คุณอาจยังคงชอบวิธีการจัดองค์ประกอบ

แต่ลักษณะนั้นน่าสนใจมากในสถานการณ์ส่วนใหญ่โดยเฉพาะอย่างยิ่งหากส่วนต่อประสานมีวิธีการมากมายซึ่งทำให้คุณเขียนแผ่นความร้อนจำนวนมาก

* คุณสามารถทำทั้งสองอย่างได้ - กำหนดEventEmitterคลาสในกรณีที่คุณต้องการใช้มันอย่างเป็นองค์ประกอบและกำหนดEventEmitterTraitลักษณะเช่นกันโดยใช้การใช้EventEmitterคลาสในลักษณะ :)


1

ลักษณะเช่นเดียวกับคลาสที่เราสามารถใช้สำหรับวัตถุประสงค์ในการสืบทอดหลายรายการและยังสามารถใช้งานโค้ดได้อีกด้วย

เราสามารถใช้คุณลักษณะภายในคลาสและเรายังสามารถใช้คุณลักษณะหลายอย่างในคลาสเดียวกันด้วย 'ใช้คำหลัก'

อินเทอร์เฟซใช้สำหรับความสามารถในการนำรหัสกลับมาได้เช่นเดียวกับคุณลักษณะ

อินเทอร์เฟซถูกขยายอินเทอร์เฟซหลายดังนั้นเราสามารถแก้ปัญหาการสืบทอดหลาย แต่เมื่อเราใช้อินเทอร์เฟซแล้วเราควรสร้างวิธีการทั้งหมดภายในชั้นเรียน สำหรับข้อมูลเพิ่มเติมคลิกลิงค์ด้านล่าง:

http://php.net/manual/en/language.oop5.traits.php http://php.net/manual/en/language.oop5.interfaces.php


1

อินเทอร์เฟซคือสัญญาที่ระบุว่า“ วัตถุนี้สามารถทำสิ่งนี้ได้” ในขณะที่คุณลักษณะนั้นให้ความสามารถในการทำสิ่งนั้นกับวัตถุ

ลักษณะเป็นหลักวิธีการ "คัดลอกและวาง" รหัสระหว่างชั้นเรียน

ลองอ่านบทความนี้ลักษณะของ PHP คืออะไร?


0

ความแตกต่างที่สำคัญคือด้วยอินเทอร์เฟซคุณต้องกำหนดการใช้งานจริงของแต่ละวิธีในแต่ละคลาสที่ใช้อินเทอร์เฟซที่กล่าวว่าดังนั้นคุณสามารถมีหลายคลาสที่ใช้อินเทอร์เฟซเดียวกัน แต่มีพฤติกรรมแตกต่างกัน ห้องเรียน; ข้อแตกต่างที่สำคัญอีกอย่างหนึ่งก็คือวิธีการตรวจสอบคุณสมบัติสามารถเป็นวิธีแบบคลาสหรือแบบสแตติกได้ซึ่งแตกต่างจากวิธีส่วนต่อประสานที่สามารถ (และมักจะ) วิธีแบบอินสแตนซ์

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.