การเขียนโปรแกรมเชิงมุมมอง: เมื่อใดที่จะเริ่มใช้เฟรมเวิร์ก


22

ฉันเพิ่งดูการสนทนานี้โดยGreg Youngเตือนผู้คนถึง KISS: Keep It Simple Stupid

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

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

ตัวอย่างที่เขามอบให้คือการกำหนดอินเตอร์เฟส:

public interface IConsumes<T>
{
    void Consume(T message);
}

ถ้าเราต้องการออกคำสั่ง:

public class Command
{
    public string SomeInformation;
    public int ID;

    public override string ToString()
    {
       return ID + " : " + SomeInformation + Environment.NewLine;
    }
}

คำสั่งถูกนำไปใช้เป็น:

public class CommandService : IConsumes<Command>
{
    private IConsumes<Command> _next;

    public CommandService(IConsumes<Command> cmd = null)
    {
        _next = cmd;
    }
    public void Consume(Command message)
    {
       Console.WriteLine("Command complete!");
        if (_next != null)
            _next.Consume(message);
    }
}

หากต้องการทำการบันทึกไปยังคอนโซลจากนั้นดำเนินการ:

public class Logger<T> : IConsumes<T>
{
    private readonly IConsumes<T> _next;

    public Logger(IConsumes<T> next)
    {
        _next = next;
    }
    public void Consume(T message)
    {
        Log(message);
        if (_next != null)
            _next.Consume(message);
    }

    private void Log(T message)
    {
        Console.WriteLine(message);
    }
}

จากนั้นการบันทึกคำสั่งล่วงหน้าบริการคำสั่งและการบันทึกคำสั่งหลังคำสั่งจะเป็นเพียง:

var log1 = new Logger<Command>(null);
var svr  = new CommandService(log);
var startOfChain = new Logger<Command>(svr);

และคำสั่งจะถูกดำเนินการโดย:

var cmd = new Command();
startOfChain.Consume(cmd);

เมื่อต้องการทำสิ่งนี้ในตัวอย่างเช่นPostSharpจะมีคำอธิบายประกอบด้วยCommandServiceวิธีนี้:

public class CommandService : IConsumes<Command>
{
    [Trace]
    public void Consume(Command message)
    {
       Console.WriteLine("Command complete!");
    }
}

และจากนั้นจะต้องใช้การบันทึกในชั้นเรียนคุณลักษณะบางอย่างเช่น:

[Serializable]
public class TraceAttribute : OnMethodBoundaryAspect
{
    public override void OnEntry( MethodExecutionArgs args )
    {
        Console.WriteLine(args.Method.Name + " : Entered!" );   
    }

    public override void OnSuccess( MethodExecutionArgs args )
    {
        Console.WriteLine(args.Method.Name + " : Exited!" );
    }

    public override void OnException( MethodExecutionArgs args )
    {
        Console.WriteLine(args.Method.Name + " : EX : " + args.Exception.Message );
    }
}

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

ดังนั้นหลังจากการสะสมค่อนข้างนานคำถามก็คือ: เมื่อไหร่ที่คุณจะเปลี่ยนจากวิธีที่ไม่ใช่กรอบของ Greg ไปสู่การใช้บางสิ่งบางอย่างเช่น PostSharp สำหรับ AOP


3
+1: คำถามที่ดีแน่นอน บางคนอาจพูดว่า "... เมื่อคุณเข้าใจวิธีการแก้ปัญหาโดยปราศจากมัน"
Steven Evers

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

@Aaraught: ใช่นั่นเป็นส่วนหนึ่งของสาเหตุที่ฉันต้องการโพสต์ที่นี่ คำอธิบายของเกร็กก็คือการกำหนดค่าระบบที่มีการเชื่อมต่อแล้วก็ขึ้นปกติทุกรหัสที่แตกต่างกันIConsumesชิ้น แทนที่จะต้องใช้ XML ภายนอกหรืออินเทอร์เฟซ Fluent บางอย่าง แต่ยังต้องเรียนรู้สิ่งอื่นอีก เราอาจโต้แย้งว่าวิธีการนี้เป็น "สิ่งที่ต้องเรียนรู้" อีกด้วย
Peter K.

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

ไม่ใช่ว่ามันจะรบกวนจิตใจฉันเลย แต่นี่จะไม่เป็นปัญหาของ Stack Overflow อีกหรือไม่
Rei Miyasaka

คำตอบ:


17

เขาพยายามเขียนกรอบ AOP "ตรงไปยัง TDWTF" หรือไม่? ฉันยังไม่ได้เบาะแสอย่างจริงจังว่าประเด็นของเขาคืออะไร ทันทีที่คุณพูดว่า "วิธีการทั้งหมดจะต้องใช้พารามิเตอร์เดียว" จากนั้นคุณล้มเหลวใช่ไหม ในขั้นตอนที่คุณพูดว่า OK สิ่งนี้มีข้อ จำกัด อย่างจริงจังต่อความสามารถของฉันในการเขียนซอฟต์แวร์เราจะวางสิ่งนี้ไว้ก่อนหน้านี้สามเดือนข้างล่างบรรทัดที่เรามี codebase ฝันร้ายที่สมบูรณ์เพื่อทำงานด้วย

และคุณรู้อะไรไหม คุณสามารถเขียนแอตทริบิวต์ง่ายขับเคลื่อน IL กรอบเข้าสู่ระบบตามค่อนข้างได้อย่างง่ายดายด้วยMono.Cecil (การทดสอบมีความซับซ้อนเล็กน้อย แต่ ... )

โอ้และ IMO หากคุณไม่ได้ใช้คุณสมบัติมันไม่ใช่ AOP จุดรวมของการทำวิธีการเข้า / ออกรหัสการบันทึกที่ขั้นตอนการโพสต์คือเพื่อที่จะไม่ยุ่งกับไฟล์รหัสของคุณและดังนั้นคุณไม่จำเป็นต้องคิดเกี่ยวกับมันในขณะที่คุณ refactor รหัสของคุณ; ที่คือพลังของมัน

เกร็กทั้งหมดได้แสดงให้เห็นว่ามีกระบวนทัศน์โง่โง่


6
+1 สำหรับเก็บมันโง่โง่ เตือนฉันถึงคำพูดที่โด่งดังของ Einstein: "ทำให้ทุกอย่างง่ายที่สุดเท่าที่จะทำได้ แต่ไม่ง่ายขึ้น"
Rei Miyasaka

FWIW, F # มีข้อ จำกัด เหมือนกันแต่ละวิธีใช้อาร์กิวเมนต์ได้สูงสุดหนึ่งรายการ
R0MANARMY

1
let concat (x : string) y = x + y;; concat "Hello, " "World!";;ดูเหมือนว่าจะใช้เวลาสองข้อโต้แย้งสิ่งที่ฉันหายไป?

2
@The Mouth - สิ่งที่เกิดขึ้นจริงคือเมื่อconcat "Hello, "คุณสร้างฟังก์ชั่นที่ใช้yงานได้จริงและมีxการกำหนดไว้ล่วงหน้าว่ามีผลผูกพันในท้องถิ่นให้เป็น "Hello" let concat_x y = "Hello, " + yถ้าฟังก์ชันกลางนี้อาจจะเห็นก็ต้องการลักษณะบางอย่างเช่น concat_x "World!"และจากนั้นต่อไปนี้ที่คุณกำลังเรียก ไวยากรณ์ทำให้มันชัดเจนน้อยลง แต่ตอนนี้ช่วยให้คุณ "อบ" ฟังก์ชั่นใหม่ - let printstrln = print "%s\n" ;; printstrln "woof"ตัวอย่างเช่น นอกจากนี้แม้ว่าคุณจะทำอะไรเช่นlet f(x,y) = x + yนั้นจริง ๆ แล้วเป็นเพียงข้อโต้แย้งทูเปิ
Rei Miyasaka

1
ครั้งแรกที่ฉันทำโปรแกรมฟังก์ชั่นใด ๆ ที่อยู่ใน Miranda กลับไปที่มหาวิทยาลัยฉันจะต้องดูที่ F # ฟังดูน่าสนใจ

8

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

ฉันไม่คิดว่าฉันจะใช้วิธีการนี้หากใช้เพื่อ AOP เท่านั้น เกร็กบอกว่ามันดีสำหรับสถานการณ์ง่าย ๆ นี่คือสิ่งที่ฉันทำในสถานการณ์ง่าย ๆ :

public void DeactivateInventoryItem(CommandServices cs, Guid item, string reason)
{
    cs.Log.Write("Deactivated: {0} ({1})", item, reason);
    repo.Deactivate(item, reason);
}

ใช่ฉันทำแล้วฉันกำจัด AOP ทั้งหมด! ทำไม? เพราะคุณไม่ต้องการ AOP ในสถานการณ์ง่าย ๆในสถานการณ์ที่เรียบง่าย

จากมุมมองการตั้งโปรแกรมการใช้งานการอนุญาตให้ใช้เพียงหนึ่งพารามิเตอร์ต่อหนึ่งฟังก์ชั่นนั้นไม่ทำให้ฉันกลัวจริงๆ อย่างไรก็ตามนี่ไม่ใช่การออกแบบที่ใช้งานได้ดีกับ C # และขัดกับภาษาของคุณไม่ได้ช่วยอะไรเลย

ฉันจะใช้วิธีการนี้เฉพาะเมื่อจำเป็นต้องสร้างแบบจำลองคำสั่งให้เริ่มด้วยเช่นถ้าฉันต้องการเลิกทำแบบกองซ้อนหรือถ้าฉันทำงานกับคำสั่ง WPFคำสั่ง

มิฉะนั้นฉันก็แค่ใช้กรอบหรือภาพสะท้อนบางอย่าง PostSharp ทำงานได้แม้ใน Silverlight และ Compact Framework - ดังนั้นสิ่งที่เขาเรียกว่า "เวทย์มนตร์" จริงๆแล้วไม่ได้วิเศษอะไรเลยเลย

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


5

ฉันเรียนอิสระในวิทยาลัยเรื่อง AOP ฉันเขียนบทความเกี่ยวกับวิธีการสร้างแบบจำลอง AOP ด้วยปลั๊กอิน Eclipse ที่จริงแล้วฉันคิดว่าไม่เกี่ยวข้องเลย ประเด็นสำคัญคือ 1) ฉันยังเด็กและไม่มีประสบการณ์และ 2) ฉันทำงานกับ AspectJ ฉันสามารถบอกคุณได้ว่า "ความมหัศจรรย์" ของกรอบ AOP ส่วนใหญ่นั้นไม่ซับซ้อน จริง ๆ แล้วฉันทำงานเกี่ยวกับโครงการในเวลาเดียวกันกับที่พยายามทำพารามิเตอร์แบบเดี่ยวโดยใช้ hashtable IMO วิธีการเดียวของพารามิเตอร์เป็นกรอบงานและเป็นสิ่งที่รุกราน แม้ในโพสต์นี้ฉันใช้เวลาพยายามทำความเข้าใจกับวิธีการพารามิเตอร์เดียวมากกว่าที่ฉันได้ทบทวนวิธีการประกาศ ฉันจะเพิ่มข้อแม้ที่ฉันไม่ได้ดูภาพยนตร์ดังนั้น "ความมหัศจรรย์" ของวิธีการนี้อาจใช้งานแอพพลิเคชั่นบางส่วน

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


+1 สำหรับ "ฉันใช้เวลาพยายามทำความเข้าใจกับวิธีการพารามิเตอร์เดียวมากกว่าที่ฉันได้ทบทวนวิธีการประกาศ" ฉันพบIConsume<T>ตัวอย่างที่ซับซ้อนมากเกินไปสำหรับสิ่งที่ประสบความสำเร็จ
Scott Whitlock

4

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

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

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


4

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

ให้การบรรยายสั้น ๆ แก่ผู้เรียนใหม่ที่อธิบายเกี่ยวกับสิ่งที่ทำงานใน AOP


4

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

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

ดังนั้นเราจึงมีบริการผลิตภัณฑ์ที่ใช้พื้นที่เก็บข้อมูล (ในกรณีนี้เราจะใช้ต้นขั้ว) บริการจะได้รับรายการผลิตภัณฑ์

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }

    public override string ToString() { return String.Format("{0}, {1}", Name, Price); }
}

public static class ProductService
{
    public static IEnumerable<Product> GetAllProducts(ProductRepositoryStub repository)
    {
        return repository.GetAll();
    }
}

public class ProductRepositoryStub
{
    public ProductRepositoryStub(string connStr) {}

    public IEnumerable<Product> GetAll()
    {
        return new List<Product>
        {
            new Product {Name = "Cd Player", Price = 49.99m},
            new Product {Name = "Yacht", Price = 2999999m }
        };
    }
}

นอกหลักสูตรคุณสามารถส่งอินเทอร์เฟซไปยังบริการได้

ต่อไปเราต้องการแสดงรายการผลิตภัณฑ์ในมุมมอง ดังนั้นเราจึงจำเป็นต้องมีส่วนต่อประสาน

public interface Handles<T>
{
    void Handle(T message);
}

และคำสั่งที่เก็บรายการผลิตภัณฑ์

public class ShowProductsCommand
{
    public IEnumerable<Product> Products { get; set; }
}

และมุมมอง

public class View : Handles<ShowProductsCommand>
{
    public void Handle(ShowProductsCommand cmd)
    {
        cmd.Products.ToList().ForEach(x => Console.WriteLine(x.ToString()));
    }
}

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

public class Application
{
    private readonly Func<IEnumerable<Product>> _getAllProducts;
    private readonly Action<ShowProductsCommand> _showProducts;

    public Application(Func<IEnumerable<Product>> getAllProducts, Action<ShowProductsCommand> showProducts)
    {
        _getAllProducts = getAllProducts;
        _showProducts = showProducts;
    }

    public void Run()
    {
        var products = _getAllProducts();
        var cmd = new ShowProductsCommand { Products = products };
        _showProducts(cmd);
    }
}

ในที่สุดเราก็เขียนใบสมัครในวิธีการหลัก

static void Main(string[] args)
{
    // composition
    Func<IEnumerable<Product>> getAllProducts = () => ProductService.GetAllProducts(new ProductRepositoryStub(""));
    Action<ShowProductsCommand> showProducts = (x) => new View().Handle(x);
    var app = new Application(getAllProducts, showProducts);

    app.Run();
}

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

public class ExceptionHandler<T> : Handles<T>
{
    private readonly Handles<T> _next;

    public ExceptionHandler(Handles<T> next) { _next = next; }

    public void Handle(T message)
    {
        try
        {
            _next.Handle(message);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

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

Action<ShowProductsCommand> showProducts = (x) => new ExceptionHandler<ShowProductsCommand>(new View()).Handle(x);

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

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

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

public class ProductQueries : Queries<IEnumerable<Product>>
{
    private readonly Func<IEnumerable<Product>> _query;

    public ProductQueries(Func<IEnumerable<Product>> query)
    {
        _query = query;
    }

    public IEnumerable<Product> Query()
    {
        return _query();
    }
}

public interface Queries<TResult>
{
    TResult Query();
}

องค์ประกอบจะต้องมีการเปลี่ยนแปลงเช่นนี้:

Func<IEnumerable<Product>> getAllProducts = () => ProductService.GetAllProducts(new ProductRepositoryStub(""));
Func<IEnumerable<Product>> queryAllProducts = new ProductQueries(getAllProducts).Query;
Action<ShowProductsCommand> showProducts = (x) => new ExceptionHandler<ShowProductsCommand>(new View()).Handle(x);
var app = new Application(queryAllProducts, showProducts);
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.