ไม่ถูกต้องใน PHP: มีเหตุผลการออกแบบ OOP หรือไม่?


16

การสืบทอดส่วนต่อประสานด้านล่างนั้นผิดกฎหมายใน PHP แต่ฉันคิดว่ามันจะมีประโยชน์พอสมควรในชีวิตจริง มี antipattern จริงหรือมีปัญหาเกี่ยวกับการออกแบบเอกสารด้านล่าง PHP นั้นปกป้องฉันหรือไม่

<?php

/**
 * Marker interface
 */
interface IConfig {}

/**
 * An api sdk tool
 */
interface IApi
{
    public __construct(IConfig $cfg);
}

/**
 * Api configuration specific to http
 */
interface IHttpConfig extends IConfig
{
    public getSomeNiceHttpSpecificFeature();
}

/**
 * Illegal, but would be really nice to have.
 * Is this not allowed by design?
 */
interface IHttpApi extends IApi
{
    /**
     * This constructor must have -exactly- the same
     * signature as IApi, even though its first argument
     * is a subtype of the parent interface's required
     * constructor parameter.
     */
    public __construct(IHttpConfig $cfg);

}

คำตอบ:


22

ลองไม่สนใจเป็นครั้งที่สองว่าวิธีการในคำถามและเรียกมันว่า__construct frobnicateทีนี้สมมติว่าคุณมีการนำวัตถุapiไปใช้IHttpApiและเป็นวัตถุconfigIHttpConfigการดำเนินการ เห็นได้ชัดว่ารหัสนี้เหมาะกับอินเตอร์เฟส:

$api->frobnicate($config)

แต่ขอสมมติว่าเรา upCast apiไปเช่นผ่านไปIApi function frobnicateTwice(IApi $api)ตอนนี้ในฟังก์ชั่นที่frobnicateเรียกว่าและตั้งแต่มันเกี่ยวข้องกับIApiมันอาจจะดำเนินการเรียกร้องดังกล่าวเป็น$api->frobnicate(new SpecificConfig(...))ที่SpecificConfigดำเนินการแต่ไม่IConfig IHttpConfigไม่มีใครทำอะไรที่ไม่น่ารังเกียจกับประเภท แต่IHttpApi::frobnicateก็มีSpecificConfigที่ที่คาดIHttpConfigไว้

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

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

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


19

ใช่สิ่งนี้ติดตามได้โดยตรงจาก Liskov ชดเชยหลักการ (LSP) เมื่อคุณแทนที่เมธอดชนิดการส่งคืนอาจมีความเฉพาะเจาะจงมากขึ้นในขณะที่ประเภทของอาร์กิวเมนต์จะต้องคงเดิมหรืออาจจะกว้างกว่า

นี่เป็นวิธีการที่ชัดเจนกว่า __constructกว่า พิจารณา:

class Vehicle {}
class Car extends Vehicle {}
class Motorcycle extends Vehicle {}

class Driver {
    public drive(Vehicle $v) { ... }
}
class CarDriver extends Driver {
    public drive(Car $c) { ... }
}

A CarDriverคือ a Driverดังนั้นCarDriverอินสแตนซ์จะต้องสามารถทำอะไรก็ได้ที่Driverทำได้ รวมทั้งการขับรถเพราะมันเป็นเพียงMotorcycles Vehicleแต่ประเภทอาร์กิวเมนต์สำหรับdriveบอกว่าCarDriverสามารถขับรถCars - ความขัดแย้ง: CarDriver ไม่สามารถDriverจะเป็นคลาสย่อยที่เหมาะสมของ

ย้อนกลับทำให้รู้สึกมากขึ้น:

class CarDriver {
    public drive(Car $c) { ... }
}
class MultiTalentedDriver extends CarDriver {
    public drive(Vehicle $v) { ... }
}

CarDriverเท่านั้นที่สามารถขับรถCars A MultiTalentedDriverสามารถขับรถCarได้เพราะCarVehicleเป็นเพียง ดังนั้นMultiTalentedDriverเป็น subclass CarDriverเหมาะสมของ

ในตัวอย่างของใด ๆ ที่สามารถสร้างกับIApi IConfigถ้าIHttpApiเป็นชนิดย่อยของIApiเราจะต้องสามารถสร้างIHttpApiใช้ใด ๆIConfigเช่น - IHttpConfigแต่ก็ยอมรับเฉพาะ นี่คือความขัดแย้ง


ผู้ขับขี่บางคนไม่สามารถขับได้ทั้งรถยนต์และรถจักรยานยนต์ ...
sakisk

3
@faif: ในสิ่งที่เป็นนามธรรมนี้พวกเขาไม่เพียง แต่สามารถทำได้ เพราะอย่างที่คุณเห็น a Driverสามารถขับได้Vehicleและเนื่องจากทั้งขยายCarและMotorcycleขยายs Vehicleทั้งหมดDriverจะต้องสามารถจัดการได้ทั้งสองอย่าง
อเล็กซ์
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.