สถาปัตยกรรมที่สะอาด: ใช้กรณีที่มีผู้นำเสนอหรือส่งคืนข้อมูลหรือไม่


42

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

พอร์ตอินพุตและเอาต์พุต

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

สถาปัตยกรรมแบบคลีนเช่นสถาปัตยกรรมหกเหลี่ยมแยกความแตกต่างระหว่างพอร์ตหลัก (เมธอด) และพอร์ตรอง (อินเทอร์เฟซที่จะใช้งานโดยอะแดปเตอร์) ตามกระแสการสื่อสารฉันคาดหวังว่า "ใช้ Case Input Port" เป็นพอร์ตหลัก (ดังนั้นเป็นเพียงวิธีการ) และ "Use Case Output Port" เป็นอินเตอร์เฟสที่จะใช้งานอาจเป็นอาร์กิวเมนต์ตัวสร้างที่ใช้อะแดปเตอร์จริง เพื่อให้ผู้โต้ตอบสามารถใช้งานได้

ตัวอย่างรหัส

ในการสร้างตัวอย่างโค้ดนี่อาจเป็นรหัสคอนโทรลเลอร์:

Presenter presenter = new Presenter();
Repository repository = new Repository();
UseCase useCase = new UseCase(presenter, repository);
useCase->doSomething();

ส่วนต่อประสานผู้นำเสนอ:

// Use Case Output Port
interface Presenter
{
    public void present(Data data);
}

ในที่สุดผู้โต้ตอบเอง:

class UseCase
{
    private Repository repository;
    private Presenter presenter;

    public UseCase(Repository repository, Presenter presenter)
    {
        this.repository = repository;
        this.presenter = presenter;
    }

    // Use Case Input Port
    public void doSomething()
    {
        Data data = this.repository.getData();
        this.presenter.present(data);
    }
}

ในการโต้ตอบเรียกผู้นำเสนอ

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

นอกจากนี้ในคำตอบสำหรับคำถามอื่นโรเบิร์ตมาร์ตินอธิบายกรณีการใช้งานที่ผู้โต้ตอบเรียกพรีเซนเตอร์ตามคำขอการอ่าน:

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

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

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

จากบทความต้นฉบับพูดคุยเกี่ยวกับ Interface Adapters

บนข้อมูลที่ส่งคืนผู้โต้ตอบ

อย่างไรก็ตามปัญหาของฉันกับวิธีนี้คือกรณีการใช้งานต้องดูแลงานนำเสนอด้วยตัวเอง ตอนนี้ฉันเห็นว่าจุดประสงค์ของPresenterอินเทอร์เฟซนั้นเป็นนามธรรมพอที่จะเป็นตัวแทนของผู้นำเสนอหลายประเภท (GUI, Web, CLI, ฯลฯ ) และมันก็หมายถึง "เอาท์พุท" ซึ่งเป็นสิ่งที่กรณีการใช้งานอาจ เป็นอย่างดีมี แต่ฉันยังไม่มั่นใจกับมันทั้งหมด

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

Repository repository = new Repository();
UseCase useCase = new UseCase(repository);
Data data = useCase.getData();
Presenter presenter = new Presenter();
presenter.present(data);

// I'm omitting the changes to the classes, which are fairly obvious

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

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

ตรงประเด็น

ดังนั้นทางเลือกใดในสองทางเลือกนี้คือการตีความ "ถูกต้อง" ของพอร์ตเคสเอาต์พุตการใช้งานตามสถาปัตยกรรมแบบคลีน? พวกเขาทั้งสองทำงานได้?


3
การข้ามการโพสต์นั้นไม่สนับสนุนอย่างยิ่ง หากนี่คือที่ที่คุณต้องการให้คำถามของคุณสดคุณควรลบออกจาก Stack Overflow
Robert Harvey

คำตอบ:


48

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

แน่นอนว่าไม่สะอาด , หัวหอม , หรือสถาปัตยกรรมหกเหลี่ยม นั่นคือนี้ :

ป้อนคำอธิบายรูปภาพที่นี่

ไม่ใช่MVC ที่จะต้องทำแบบนั้น

ป้อนคำอธิบายรูปภาพที่นี่

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

บางวิธีเหล่านั้นได้รับชื่อแตกต่างกัน : ป้อนคำอธิบายรูปภาพที่นี่

และทุกคนสามารถเรียก MVC ได้อย่างสมเหตุสมผล

อย่างไรก็ตามไม่มีผู้ใดที่จับภาพสิ่งที่สถาปัตยกรรม buzzword (Clean, Onion และ Hex) ทั้งหมดขอให้คุณทำ

ป้อนคำอธิบายรูปภาพที่นี่

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

ป้อนคำอธิบายรูปภาพที่นี่

สิ่งหนึ่งที่ควรชัดเจนในที่นี้คือโมเดลการตอบสนองไม่ได้เดินผ่านตัวควบคุม

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

สงสัยว่าพวกเขาพลิกกลับหัวลงเพื่อให้การไหลของการควบคุมผ่านตามเข็มนาฬิกาหรือไม่ เพิ่มเติมเกี่ยวกับสิ่งนั้นและหัวลูกศร "สีขาว" เหล่านี้ในภายหลัง

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

เนื่องจากการสื่อสารจาก Controller to Presenter นั้นหมายถึงการผ่านแอปพลิเคชั่น "เลเยอร์" ดังนั้นการทำให้ตัวควบคุมทำส่วนหนึ่งของงานผู้นำเสนออาจรั่วไหล นี่คือการวิจารณ์หัวหน้าของฉันสถาปัตยกรรม VIPER

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

พอร์ตอินพุตและเอาต์พุต

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

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

สถาปัตยกรรมแบบคลีนเช่นสถาปัตยกรรมหกเหลี่ยมแยกความแตกต่างระหว่างพอร์ตหลัก (เมธอด) และพอร์ตรอง (อินเทอร์เฟซที่จะใช้งานโดยอะแดปเตอร์) ตามกระแสการสื่อสารฉันคาดหวังว่า "ใช้ Case Input Port" เป็นพอร์ตหลัก (ดังนั้นเป็นเพียงวิธีการ) และ "Use Case Output Port" เป็นอินเตอร์เฟสที่จะใช้งานอาจเป็นอาร์กิวเมนต์ตัวสร้างที่ใช้อะแดปเตอร์จริง เพื่อให้ผู้โต้ตอบสามารถใช้งานได้

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

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

การปฏิบัติตามกฎเหล่านี้จะรักษาความคิดที่ว่าชั้นแอปพลิเคชันหรือชั้นในใด ๆ ไม่รู้อะไรเลยเกี่ยวกับชั้นนอก


ในการโต้ตอบเรียกผู้นำเสนอ

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

สิ่งสำคัญเกี่ยวกับลูกศร "สีขาว" คือมันช่วยให้คุณทำสิ่งนี้:

ป้อนคำอธิบายรูปภาพที่นี่

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

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

ทำไมการกลับทิศทางของการพึ่งพาว่าเป็นสิ่งสำคัญเพื่อให้สามารถเรียนรู้โดยการศึกษาการพึ่งพาผกผันหลักการ ฉันแมปบนหลักการที่ว่าแผนภาพเหล่านี้ที่นี่

บนข้อมูลที่ส่งคืนผู้โต้ตอบ

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

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

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

Repository repository = new Repository();
UseCase useCase = new UseCase(repository);
Data data = useCase.getData();
Presenter presenter = new Presenter();
presenter.present(data);
// I'm omitting the changes to the classes, which are fairly obvious

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

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

อีกครั้งโปรดศึกษาการแยกความรับผิดชอบในการค้นหาคำสั่งเพื่อดูสาเหตุที่สำคัญ

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

ใช่ การบอกไม่ได้ถามจะช่วยให้วัตถุนี้มุ่งเน้นมากกว่าขั้นตอน

ตรงประเด็น

ดังนั้นทางเลือกใดในสองทางเลือกนี้คือการตีความ "ถูกต้อง" ของพอร์ตเคสเอาต์พุตการใช้งานตามสถาปัตยกรรมแบบคลีน? พวกเขาทั้งสองทำงานได้?

สิ่งใดก็ได้ที่ใช้ได้จริง แต่ฉันจะไม่บอกว่าตัวเลือกที่สองที่คุณนำเสนอนั้นเป็นไปตาม Clean Architecture อย่างซื่อสัตย์ มันอาจเป็นสิ่งที่ใช้งานได้ แต่มันไม่ใช่สิ่งที่ Clean Architecture ขอ


4
ขอขอบคุณที่สละเวลาเขียนคำอธิบายเชิงลึกดังกล่าว
swahnee

1
ฉันพยายามคลุมศีรษะด้วย Clean Architecture และคำตอบนี้เป็นทรัพยากรที่ยอดเยี่ยม ทำได้ดีมาก!
นาธาน

คำตอบที่ยอดเยี่ยมและมีรายละเอียด .. ขอบคุณสำหรับสิ่งนั้น .. คุณสามารถให้คำแนะนำ (หรือชี้ไปที่คำอธิบาย) เกี่ยวกับการอัปเดต GUI ระหว่างการใช้งาน CaseCase ได้หรือไม่นั่นคือแถบความคืบหน้าในขณะอัปโหลดไฟล์ขนาดใหญ่
Ewoks

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

7

ในการอภิปรายที่เกี่ยวข้องกับคำถามของคุณลุงบ๊อบอธิบายวัตถุประสงค์ของผู้นำเสนอในสถาปัตยกรรมที่สะอาดของเขา:

รับตัวอย่างรหัสนี้:

namespace Some\Controller;

class UserController extends Controller {
    public function registerAction() {
        // Build the Request object
        $request = new RegisterRequest();
        $request->name = $this->getRequest()->get('username');
        $request->pass = $this->getRequest()->get('password');

        // Build the Interactor
        $usecase = new RegisterUser();

        // Execute the Interactors method and retrieve the response
        $response = $usecase->register($request);

        // Pass the result to the view
        $this->render(
            '/user/registration/template.html.twig', 
            array('id' =>  $response->getId()
        );
    }
}

ลุงบ็อบพูดสิ่งนี้:

" วัตถุประสงค์ของผู้นำเสนอคือแยกการใช้เคสจากรูปแบบของ UI ในตัวอย่างของคุณตัวแปร $ response ถูกสร้างขึ้นโดยผู้โต้ตอบ แต่ถูกใช้โดยมุมมองนี่เป็นการโต้ตอบกับมุมมองตัวอย่างเช่น สมมติว่าหนึ่งในเขตข้อมูลในวัตถุการตอบสนอง $ เป็นวันที่เขตข้อมูลนั้นจะเป็นวัตถุวันที่ไบนารีที่สามารถแสดงผลในรูปแบบวันที่ที่แตกต่างกันจำนวนมากต้องการรูปแบบวันที่ที่เฉพาะเจาะจงมากอาจ DD / MM / YYYY มันเป็นความรับผิดชอบของใครในการสร้างรูปแบบถ้าผู้โต้ตอบสร้างรูปแบบนั้นมันจะรู้มากเกินไปเกี่ยวกับมุมมอง แต่ถ้ามุมมองใช้วัตถุวันที่แบบไบนารี่แล้วมันก็รู้มากเกี่ยวกับผู้ปฏิสัมพันธ์

"งานของผู้นำเสนอคือ ข้อมูลจากวัตถุตอบกลับและจัดรูปแบบสำหรับมุมมอง ทั้งมุมมองและผู้โต้ตอบไม่ทราบเกี่ยวกับรูปแบบของกันและกัน "

--- ลุงบ๊อบ

(อัปเดต: 31 พฤษภาคม 2019)

จากคำตอบของลุงบ๊อบฉันคิดว่ามันไม่สำคัญว่าเราจะทำข้อเลือก # 1 (ให้ผู้ใช้ใช้งานผู้โต้ตอบหรือไม่) ...

class UseCase
{
    private Presenter presenter;
    private Repository repository;

    public UseCase(Repository repository, Presenter presenter)
    {
        this.presenter = presenter;
        this.repository = repository;
    }

    public void Execute(Request request)
    {
        ...
        Response response = new Response() {...}
        this.presenter.Show(response);
    }
}

... หรือเราเลือก # 2 (ให้ผู้ตอบกลับโต้ตอบตอบกลับสร้างผู้นำเสนอภายในตัวควบคุมจากนั้นผ่านการตอบกลับไปยังผู้นำเสนอ) ...

class Controller
{
    public void ExecuteUseCase(Data data)
    {
        Request request = ...
        UseCase useCase = new UseCase(repository);
        Response response = useCase.Execute(request);
        Presenter presenter = new Presenter();
        presenter.Show(response);
    }
}

โดยส่วนตัวแล้วฉันชอบตัวเลือก # 1เพราะฉันต้องการที่จะสามารถควบคุมภายในinteractor เวลาที่จะแสดงข้อมูลและข้อความแสดงข้อผิดพลาดเช่นตัวอย่างด้านล่าง:

class UseCase
{
    private Presenter presenter;
    private Repository repository;

    public UseCase(Repository repository, Presenter presenter)
    {
        this.presenter = presenter;
        this.repository = repository;
    }

    public void Execute(Request request)
    {
        if (<invalid request>) 
        {
            this.presenter.ShowError("...");
            return;
        }

        if (<there is another error>) 
        {
            this.presenter.ShowError("another error...");
            return;
        }

        ...
        Response response = new Response() {...}
        this.presenter.Show(response);
    }
}

... ฉันต้องการที่จะสามารถทำสิ่งเหล่านี้if/elseที่เกี่ยวข้องกับการนำเสนอภายในinteractorและไม่ได้อยู่นอกปฏิสัมพันธ์

ถ้าในขณะที่เราทำ # 2 ตัวเลือกเราจะมีการจัดเก็บข้อผิดพลาด (s) ในresponseวัตถุกลับไปที่responseวัตถุจากinteractorไปcontrollerและทำการcontroller แยกresponseวัตถุ ...

class UseCase
{
    public Response Execute(Request request)
    {
        Response response = new Response();
        if (<invalid request>) 
        {
            response.AddError("...");
        }

        if (<there is another error>) 
        {
            response.AddError("another error...");
        }

        if (response.HasNoErrors)
        {
            response.Whatever = ...
        }

        ...
        return response;
    }
}
class Controller
{
    private UseCase useCase;

    public Controller(UseCase useCase)
    {
        this.useCase = useCase;
    }

    public void ExecuteUseCase(Data data)
    {
        Request request = new Request() 
        {
            Whatever = data.whatever,
        };
        Response response = useCase.Execute(request);
        Presenter presenter = new Presenter();
        if (response.ErrorMessages.Count > 0)
        {
            if (response.ErrorMessages.Contains(<invalid request>))
            {
                presenter.ShowError("...");
            }
            else if (response.ErrorMessages.Contains("another error")
            {
                presenter.ShowError("another error...");
            }
        }
        else
        {
            presenter.Show(response);
        }
    }
}

ฉันไม่ชอบการแยกresponseข้อมูลสำหรับข้อผิดพลาดภายในcontrollerเพราะถ้าเราทำอย่างนั้นเราจะทำผลงานที่ซ้ำซ้อน --- ถ้าเราเปลี่ยนแปลงอะไรบางอย่างในนี้เรายังมีการเปลี่ยนแปลงบางสิ่งบางอย่างในinteractorcontroller

นอกจากนี้หากเราตัดสินใจที่จะนำข้อมูลของเรากลับมาใช้อีกครั้งในภายหลังinteractorโดยใช้คอนโซลเราต้องจำไว้ว่าให้คัดลอกวางทั้งหมดif/elseในcontrollerแอปคอนโซลของเรา

// in the controller for our console app
if (response.ErrorMessages.Count > 0)
{
    if (response.ErrorMessages.Contains(<invalid request>))
    {
        presenterForConsole.ShowError("...");
    }
    else if (response.ErrorMessages.Contains("another error")
    {
        presenterForConsole.ShowError("another error...");
    }
}
else
{
    presenterForConsole.Present(response);
}

ถ้าเราใช้ตัวเลือกอันดับ 1 ของเราจะมีนี้if/else เฉพาะในสถานที่หนึ่งinteractorที่:


หากคุณใช้ ASP.NET MVC (หรือกรอบงาน MVC อื่น ๆ ที่คล้ายกัน) ตัวเลือก # 2 เป็นวิธีที่ง่ายกว่า

แต่เรายังสามารถทำข้อเลือก # 1 ในสภาพแวดล้อมแบบนั้นได้ นี่คือตัวอย่างของการทำตัวเลือก # 1 ใน ASP.NET MVC:

(ขอให้สังเกตว่าเราจำเป็นต้องมีpublic IActionResult Resultในผู้นำเสนอของแอป ASP.NET MVC ของเรา)

class UseCase
{
    private Repository repository;

    public UseCase(Repository repository)
    {
        this.repository = repository;
    }

    public void Execute(Request request, Presenter presenter)
    {
        if (<invalid request>) 
        {
            this.presenter.ShowError("...");
            return;
        }

        if (<there is another error>) 
        {
            this.presenter.ShowError("another error...");
            return;
        }

        ...
        Response response = new Response() {
            ...
        }
        this.presenter.Show(response);
    }
}
// controller for ASP.NET app

class AspNetController
{
    private UseCase useCase;

    public AspNetController(UseCase useCase)
    {
        this.useCase = useCase;
    }

    [HttpPost("dosomething")]
    public void ExecuteUseCase(Data data)
    {
        Request request = new Request() 
        {
            Whatever = data.whatever,
        };
        var presenter = new AspNetPresenter();
        useCase.Execute(request, presenter);
        return presenter.Result;
    }
}
// presenter for ASP.NET app

public class AspNetPresenter
{
    public IActionResult Result { get; private set; }

    public AspNetPresenter(...)
    {
    }

    public async void Show(Response response)
    {
        Result = new OkObjectResult(new { });
    }

    public void ShowError(string errorMessage)
    {
        Result = new BadRequestObjectResult(errorMessage);
    }
}

(ขอให้สังเกตว่าเราจำเป็นต้องมีpublic IActionResult Resultในผู้นำเสนอของแอป ASP.NET MVC ของเรา)

หากเราตัดสินใจที่จะสร้างแอปอื่นสำหรับคอนโซลเราสามารถนำมาใช้ใหม่UseCaseด้านบนและสร้างเพียงแค่ControllerและPresenterสำหรับคอนโซล:

// controller for console app

class ConsoleController
{    
    public void ExecuteUseCase(Data data)
    {
        Request request = new Request() 
        {
            Whatever = data.whatever,
        };
        var presenter = new ConsolePresenter();
        useCase.Execute(request, presenter);
    }
}
// presenter for console app

public class ConsolePresenter
{
    public ConsolePresenter(...)
    {
    }

    public async void Show(Response response)
    {
        // write response to console
    }

    public void ShowError(string errorMessage)
    {
        Console.WriteLine("Error: " + errorMessage);
    }
}

(โปรดสังเกตว่าเราไม่ได้public IActionResult Resultอยู่ในพรีเซนเตอร์ของแอปคอนโซลของเรา)


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

ฉันขอโทษ. อาจจะสับสนเพราะฉันไม่ได้รวมตัวอย่างโค้ดจากการสนทนา ฉันจะอัปเดตเพื่อรวมตัวอย่างรหัส
Jboy Flaga

ลุงบ๊อบไม่ได้พูดว่าผู้โต้ตอบไม่ควรสร้างการโต้ตอบ การตอบสนองจะถูกสร้างขึ้นโดยการโต้ตอบด้วย สิ่งที่ลุงบ็อบพูดคือการตอบสนองที่สร้างโดยผู้โต้ตอบจะถูกใช้โดยผู้นำเสนอ จากนั้นผู้นำเสนอจะ "จัดรูปแบบ" นำการตอบสนองที่จัดรูปแบบแล้วไปยัง viewmodel แล้วส่งผ่าน viewmodel นั้นไปยังมุมมอง <br/> นั่นคือสิ่งที่ฉันเข้าใจ
Jboy Flaga

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

2

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

มาทำความเข้าใจกับคำศัพท์สองสามข้อก่อนที่จะทำความเข้าใจกับแอพพลิเคชั่นต่าง ๆ :

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

กรณีการใช้งานที่มีข้อมูลที่ส่งคืน

ในกรณีปกติกรณีการใช้งานจะส่งคืนวัตถุโดเมนไปยังชั้นแอปพลิเคชันที่สามารถประมวลผลเพิ่มเติมในชั้นแอปพลิเคชันเพื่อให้แสดงเป็นมิตรใน UI

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

นี่คือตัวอย่างโค้ดแบบง่าย ๆ :

namespace SimpleCleanArchitecture
{
    public class OutputDTO
    {
        //fields
    }

    public class Presenter 
    {
        public OutputDTO Present(Domain domain)
        {
            // Mapping takes action. Dummy object returned for demonstration purpose
            // Usually frameworks like automapper to the mapping job.
            return new OutputDTO();
        }
    }

    public class Domain
    {
        //fields
    }

    public class UseCaseInteractor
    {
        public Domain Process(Domain domain)
        {
            // additional processing takes place here
            return domain;
        }
    }

    // A simple controller. 
    // Usually frameworks like asp.net mvc provides url routing mechanism to reach here through this type of class.
    public class Controller
    {
        public View Action()
        {
            UseCaseInteractor userCase = new UseCaseInteractor();
            var domain = userCase.Process(new Domain());//passing dummy domain(for demonstration purpose) to process
            var presenter = new Presenter();//presenter might be initiated via dependency injection.

            return new View(presenter.Present(domain));
        }
    }

    // A simple view. 
    // Usually frameworks like asp.net mvc provides mechanism to render html based view through this type of class.
    public class View
    {
        OutputDTO _outputDTO;

        public View(OutputDTO outputDTO)
        {
            _outputDTO = outputDTO;
        }

    }
}

การใช้ Case บรรจุที่มีผู้นำเสนอ

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

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

ป้อนคำอธิบายรูปภาพที่นี่

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

namespace CleanArchitectureWithPresenterInUseCase
{
    public class Domain
    {
        //fields
    }

    public class OutputDTO
    {
        //fields
    }

    // Use Case Output Port
    public interface IPresenter
    {
        OutputDTO Present(Domain domain);
    }

    public class Presenter: IPresenter
    {
        public OutputDTO Present(Domain domain)
        {
            // Mapping takes action. Dummy object returned for demonstration purpose
            // Usually frameworks like automapper to the mapping job.
            return new OutputDTO();
        }
    }

    // Use Case Input Port / Interactor   
    public class UseCaseInteractor
    {
        IPresenter _presenter;
        public UseCaseInteractor (IPresenter presenter)
        {
            _presenter = presenter;
        }

        public OutputDTO Process(Domain domain)
        {
            return _presenter.Present(domain);
        }
    }

    // A simple controller. 
    // Usually frameworks like asp.net mvc provides url routing mechanism to reach here through this type of class.
    public class Controller
    {
        public View Action()
        {
            IPresenter presenter = new Presenter();//presenter might be initiated via dependency injection.
            UseCaseInteractor userCase = new UseCaseInteractor(presenter);
            var outputDTO = userCase.Process(new Domain());//passing dummy domain (for demonstration purpose) to process
            return new View(outputDTO);
        }
    }

    // A simple view. 
    // Usually frameworks like asp.net mvc provides mechanism to render html based view through this type of class.
    public class View
    {
        OutputDTO _outputDTO;

        public View(OutputDTO outputDTO)
        {
            _outputDTO = outputDTO;
        }

    }
}

1

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

ตัวอย่างนี้เป็นวิธีที่ง่ายในการใช้แนวคิดของ Clean Architecture (Dependency Rule) ในบริบทของ Asp.Net MVC

ฉันเขียนโพสต์บล็อกเพื่อดำน้ำลึกลงในการสนทนานี้: https://plainionist.github.io/Implementing-Clean-Architecture-Controller-Presenter/


1

ใช้กรณีที่มีผู้นำเสนอหรือส่งคืนข้อมูลหรือไม่

ดังนั้นทางเลือกใดในสองทางเลือกนี้คือการตีความ "ถูกต้อง" ของพอร์ตเคสเอาต์พุตการใช้งานตามสถาปัตยกรรมแบบคลีน? พวกเขาทั้งสองทำงานได้?


ในระยะสั้น

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

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

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

แผนภาพคลาส UML ของ Uncle Bob ของ Clean Architecture


สองเซ็นต์ของฉัน

เหตุผลหลักว่าทำไมฉันชอบกลับUseCaseResponseเป็นว่าวิธีการนี้ช่วยให้กรณีการใช้งานของฉันมีความยืดหยุ่นช่วยให้ทั้งสององค์ประกอบระหว่างพวกเขาและgenericity ( ทั่วไปและเฉพาะรุ่น ) ตัวอย่างพื้นฐาน:

// A generic "entity type agnostic" use case encapsulating the interaction logic itself.
class UpdateUseCase implements UpdateUseCaseInterface
{
    function __construct(EntityGatewayInterface $entityGateway, GetUseCaseInterface $getUseCase)
    {
        $this->entityGateway = $entityGateway;
        $this->getUseCase = $getUseCase;
    }

    public function execute(UpdateUseCaseRequestInterface $request) : UpdateUseCaseResponseInterface
    {
        $getUseCaseResponse = $this->getUseCase->execute($request);

        // Update the entity and build the response...

        return $response;
    }
}

// "entity type aware" use cases encapsulating the interaction logic WITH the specific entity type.
final class UpdatePostUseCase extends UpdateUseCase;
final class UpdateProductUseCase extends UpdateUseCase;

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


บนข้อมูลที่ส่งคืนผู้โต้ตอบ

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

ไม่แน่ใจว่าจะเข้าใจสิ่งที่คุณหมายถึงอะไรทำไมคุณต้อง "ควบคุม" การเสนอข้อมูล? คุณไม่สามารถควบคุมมันได้ตราบใดที่คุณไม่ส่งคืนการตอบสนองกรณีใช้งาน

กรณีใช้งานสามารถส่งคืนรหัสสถานะเพื่อตอบกลับไปยังเลเยอร์ไคลเอนต์ว่าเกิดอะไรขึ้นระหว่างการดำเนินการ รหัสสถานะการตอบกลับ HTTP นั้นเหมาะสมอย่างยิ่งที่จะอธิบายสถานะการทำงานของกรณีการใช้ ...

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