DDD บริการฉีดในวิธีการนิติบุคคลที่เรียก


11

คำถามแบบสั้น

มันอยู่ในแนวปฏิบัติที่ดีที่สุดของ DDD และ OOP ในการฉีดบริการกับการเรียกเมธอดเอนทิตีหรือไม่?

ตัวอย่างรูปแบบยาว

สมมติว่าเรามีกรณี Order-LineItems แบบคลาสสิกใน DDD ที่เรามี Domain Entity ชื่อ Order ซึ่งยังทำหน้าที่เป็น Aggregate Root และ Entity นั้นไม่เพียง แต่ประกอบไปด้วย Value Objects แต่ยังเป็นคอลเลกชันของรายการโฆษณา หน่วยงาน

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

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

เราไม่ต้องการฉีด LineItemRepository ใด ๆ ลงใน OrderEntity เนื่องจากเป็นการละเมิดหลักการหลายอย่างที่ฉันสามารถนึกได้ แต่ความคล่องแคล่วของวากยสัมพันธ์เป็นสิ่งที่เราต้องการจริงๆเพราะมันง่ายต่อการอ่านและบำรุงรักษารวมถึงการทดสอบ

พิจารณาโค้ดต่อไปนี้โดยสังเกตวิธีgetLineItemsในOrderEntity:

interface IOrderService {
    public function getOrderByID($orderID) : OrderEntity;
    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection;
}

class OrderService implements IOrderService {
    private $orderRepository;
    private $lineItemRepository;

    public function __construct(IOrderRepository $orderRepository, ILineItemRepository $lineItemRepository) {
        $this->orderRepository = $orderRepository;
        $this->lineItemRepository = $lineItemRepository;
    }

    public function getOrderByID($orderID) : OrderEntity {
        return $this->orderRepository->getByID($orderID);
    }

    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection {
        return $this->lineItemRepository->getLineItemsByOrderID($orderEntity->ID());
    }
}

class OrderEntity {
    private $ID;
    private $lineItems;

    public function getLineItems(IOrderServiceInternal $orderService) {
        if(!is_null($this->lineItems)) {
            $this->lineItems = $orderService->getLineItems($this);
        }
        return $this->lineItems;
    }
}

นั่นเป็นวิธีที่ได้รับการยอมรับในการใช้งานไวยากรณ์ได้อย่างคล่องแคล่วในกิจการโดยไม่ละเมิดหลักการหลักของ DDD และ OOP หรือไม่? สำหรับฉันดูเหมือนว่าใช้ได้เพราะเราเปิดเผยเฉพาะเลเยอร์บริการไม่ใช่เลเยอร์โครงสร้างพื้นฐาน (ที่ซ้อนอยู่ภายในบริการ)

คำตอบ:


9

มันเป็นเรื่องที่ดีโดยสิ้นเชิงที่จะผ่านบริการโดเมนในการโทรนิติบุคคล สมมติว่าเราต้องคำนวณผลรวมของใบแจ้งหนี้ด้วยอัลกอริทึมที่ซับซ้อนซึ่งขึ้นอยู่กับประเภทลูกค้า นี่คือสิ่งที่ดูเหมือนว่า:

class Invoice
{
    private $currency;
    private $customerId;

    public function __construct()
    {
    }

    public function sum(InvoiceCalculator $calculator)
    {
        $sum =
            new SumRecord(
                $calculator->calculate($this)
            )
        ;

        if ($sum->isZero()) {
            $this->events->add(new ZeroSumCalculated());
        }

        return $sum;
    }
}

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

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


6

ฉันตกใจที่อ่านคำตอบบางส่วนที่นี่

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

สิ่งที่ไม่ถูกต้องคือการฉีดบริการเป็นจำนวนมากผ่าน Constructor แต่ด้วยวิธีการทางธุรกิจก็โอเคและปกติอย่างสมบูรณ์แบบ


1
เหตุใดกรณีที่คุณให้ไม่เป็นความรับผิดชอบของบริการโดเมน
e_i_pi

1
มันเป็นบริการโดเมน แต่มันถูกฉีดในวิธีการทางธุรกิจ ชั้นการประยุกต์ใช้เป็นเพียงประพันธ์,
diegosasw

ฉันไม่ได้รับประสบการณ์ใน DDD แต่ไม่ควรเรียกใช้บริการโดเมนจาก Application Service และหลังจากการตรวจสอบความถูกต้องของบริการโดเมนยังคงเรียกวิธีการของ Entity ผ่าน Application Service นั้น ฉันกำลังเผชิญปัญหาเดียวกันในโครงการของฉันเพราะบริการโดเมนเรียกใช้การโทรฐานข้อมูลผ่านที่เก็บข้อมูล ... ฉันไม่รู้ว่ามันจะถูก
Muflix

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

5

มันอยู่ในแนวปฏิบัติที่ดีที่สุดของ DDD และ OOP ในการฉีดบริการกับการเรียกเมธอดเอนทิตีหรือไม่?

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

$order->getLineItems($orderService)

สิ่งนี้เป็นความผิดพลาดโดยรวมไม่ควรต้องการสิ่งใดนอกจากตัวเองเพื่อส่งคืนรายการสั่งซื้อ ทั้งหมดรวมควรจะโหลดแล้วก่อนที่จะเรียกวิธีการ หากคุณรู้สึกว่าควรโหลดแบบขี้เกียจนี่เป็นความเป็นไปได้สองอย่าง:

  1. ขอบเขตการรวมของคุณผิดพวกเขาใหญ่เกินไป

  2. ใน usecase นี้คุณใช้การรวมเท่านั้นสำหรับการอ่าน ทางออกที่ดีที่สุดคือการแยกโมเดลการเขียนออกจากโมเดลการอ่าน (เช่นใช้CQRS ) ในสถาปัตยกรรมที่สะอาดกว่านี้คุณไม่ได้รับอนุญาตให้ทำการค้นหาแบบรวม แต่เป็นแบบจำลองการอ่าน


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

1
@Muflix ใช่ถูกต้องแล้ว
Constantin Galbenu

3

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

รูทรวมการสั่งซื้อจะไม่ให้การอ้างอิงถึงคอลเลกชัน lineitem ที่จะช่วยให้คุณสามารถแก้ไขคอลเลกชันและจะไม่ให้การอ้างอิงคอลเลกชันของรายการโฆษณาใด ๆ ที่จะช่วยให้คุณแก้ไข หากคุณต้องการเปลี่ยนการรวมคำสั่งซื้อหลักการของฮอลลีวูดจะนำไปใช้: "บอกไม่ต้องถาม"

การส่งคืนค่าจากภายในการรวมเป็นการปรับเนื่องจากค่านั้นไม่เปลี่ยนรูปโดยเนื้อแท้ คุณไม่สามารถเปลี่ยนแปลงข้อมูลของฉันได้โดยเปลี่ยนสำเนาของคุณ

การใช้บริการโดเมนเป็นอาร์กิวเมนต์เพื่อช่วยในการรวบรวมค่าที่ถูกต้องเป็นสิ่งที่สมเหตุสมผลอย่างสมบูรณ์

ตามปกติคุณจะไม่ใช้บริการโดเมนเพื่อให้การเข้าถึงข้อมูลที่อยู่ในการรวมเนื่องจากการรวมควรมีการเข้าถึง

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

ดังนั้นการสะกดคำนั้นแปลกถ้าเราพยายามเข้าถึงการรวบรวมค่าของรายการโฆษณา การสะกดที่เป็นธรรมชาติมากขึ้นจะเป็น

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
  ...
}

แน่นอนว่าสิ่งนี้เป็นการล่วงหน้าว่ามีการโหลดรายการโฆษณาแล้ว

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

วิธีการนี้ไม่ใช่สิ่งที่คุณจะพบใน Evans ดั้งเดิมซึ่งเขาคิดว่าการรวมจะมีรูปแบบข้อมูลเดียวที่เชื่อมโยงกับมัน มันออกมาเป็นธรรมชาติมากขึ้นจาก CQRS


ขอบคุณสำหรับสิ่งนี้. ตอนนี้ฉันได้อ่านประมาณครึ่งหนึ่งของ "หนังสือสีแดง" และฉันได้ลองใช้หลักการฮอลลีวูดเป็นครั้งแรกในชั้นโครงสร้างพื้นฐาน อ่านคำตอบทั้งหมดเหล่านี้อีกครั้งพวกเขาทุกคนให้คะแนนที่ดี แต่ฉันคิดว่าคุณมีประเด็นที่สำคัญมากเกี่ยวกับขอบเขตlineItems()และการโหลดล่วงหน้าเมื่อมีการเรียกใช้ Aggregate Root ครั้งแรก
e_i_pi

3

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

เกี่ยวกับการใช้งานโครงสร้างพื้นฐานของ OrderRepository ในกรณี ORM ความสัมพันธ์แบบหนึ่งต่อหลายคนและคุณสามารถเลือกโหลด OrderLine อย่างกระตือรือร้นหรือขี้เกียจก็ได้

ฉันไม่แน่ใจว่าบริการของคุณหมายถึงอะไรกันแน่ มันค่อนข้างใกล้เคียงกับ "Application Service" หากเป็นกรณีนี้โดยทั่วไปแล้วไม่ใช่ความคิดที่ดีที่จะฉีดเซอร์วิสไปยัง Aggregate root / Entity / Object Object บริการแอปพลิเคชันควรเป็นไคลเอนต์ของ Aggregate Root / Entity / Object Object และ Domain Service อีกสิ่งหนึ่งเกี่ยวกับบริการของคุณคือการเปิดเผยวัตถุที่มีค่าใน Application Service ไม่ใช่ความคิดที่ดี ควรเข้าถึงได้โดยรูทรวม


2

คำตอบคือ: ไม่แน่นอนหลีกเลี่ยงการส่งผ่านบริการในวิธีเอนทิตี

การแก้ปัญหานั้นง่าย: เพียงให้ที่เก็บคำสั่งซื้อคืนคำสั่งซื้อที่มี LineItems ทั้งหมด ในกรณีของคุณการรวมคือ Order + LineItems ดังนั้นหากที่เก็บไม่ส่งคืนผลรวมทั้งหมดแสดงว่าการรวมนั้นไม่ได้ทำงาน

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

อีกอย่างหนึ่ง: ถ้าเป็นไปได้พยายามหลีกเลี่ยงการทำสิ่งนี้:

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
  ...
}

ทำสิ่งนี้แทน

$order = $orderService->getOrderByID($orderID);
$order->doSomethingSignificant();

ในการออกแบบเชิงวัตถุเราพยายามหลีกเลี่ยงการตกปลาในข้อมูลวัตถุ เราชอบที่จะขอให้วัตถุทำในสิ่งที่เราต้องการ

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