Lập trình hướng theo khía cạnh: Khi nào bắt đầu sử dụng một khung công tác?


22

Tôi vừa xem bài nói chuyện này của Greg Young cảnh báo mọi người về KISS: Keep It Simple St ngu.

Một trong những điều ông đề xuất là để lập trình hướng theo khía cạnh, người ta không cần một khuôn khổ .

Anh ta bắt đầu bằng cách thực hiện một ràng buộc mạnh mẽ: rằng tất cả các phương thức đều lấy một và chỉ một tham số (mặc dù anh ta thư giãn điều này một lát sau bằng cách sử dụng một phần ứng dụng ).

Ví dụ anh đưa ra là định nghĩa một giao diện:

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

Nếu chúng ta muốn đưa ra một lệnh:

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

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

Lệnh được thực hiện như:

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);
    }
}

Để thực hiện đăng nhập vào bàn điều khiển, người ta chỉ cần thực hiện:

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);
    }
}

Sau đó, ghi nhật ký lệnh trước, dịch vụ lệnh và ghi nhật ký lệnh sau đó chỉ là:

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

và lệnh được thực thi bởi:

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

Để làm điều này, ví dụ, PostSharp , người ta sẽ chú thích CommandServicetheo cách này:

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

Và sau đó phải thực hiện đăng nhập trong một lớp thuộc tính như:

[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 );
    }
}

Đối số mà Greg sử dụng là kết nối từ thuộc tính đến việc thực hiện thuộc tính là "quá nhiều phép thuật" để có thể giải thích những gì đang xảy ra với một nhà phát triển cơ sở. Ví dụ ban đầu là tất cả "chỉ mã" và dễ dàng giải thích.

Vì vậy, sau quá trình xây dựng khá dài, câu hỏi đặt ra là: khi nào bạn thực hiện chuyển đổi từ cách tiếp cận phi khung của Greg sang sử dụng một cái gì đó như PostSharp cho AOP?


3
+1: Chắc chắn là một câu hỏi hay. Người ta có thể chỉ cần nói "... khi bạn đã hiểu giải pháp mà không có nó."
Steven Evers

1
Có lẽ tôi chỉ không quen với phong cách này, nhưng ý tưởng viết toàn bộ ứng dụng như thế này khiến tôi hoàn toàn điên rồ. Tôi muốn sử dụng một phương thức đánh chặn.
Aaronaught

@Aaronaught: Vâng, đó là một phần lý do tại sao tôi muốn đăng ở đây. Giải thích của Greg là cấu hình hệ thống sau đó chỉ kết nối với MÃ BÌNH THƯỜNG tất cả các phần khác nhau IConsumes. Thay vì phải sử dụng XML bên ngoài hoặc một số giao diện Fluent --- còn một điều nữa cần tìm hiểu. Người ta có thể lập luận rằng phương pháp này cũng là "một thứ khác để học".
Peter K.

Tôi vẫn không chắc mình hiểu động lực; bản chất của các khái niệm như AOP là có thể thể hiện mối quan tâm một cách khai báo , tức là thông qua cấu hình. Đối với tôi đây chỉ là phát minh lại bánh xe vuông. Không chỉ trích bạn hay câu hỏi của bạn, nhưng tôi nghĩ câu trả lời hợp lý duy nhất là "Tôi sẽ không bao giờ sử dụng phương pháp của Greg trừ khi mọi lựa chọn khác đều thất bại."
Aaronaught

Không phải điều đó làm phiền tôi chút nào, nhưng đây có phải là một câu hỏi về Stack Overflow không?
Rei Miyasaka

Câu trả lời:


17

Có phải anh ta đang cố gắng viết một khung AOP "thẳng tới TDWTF"? Tôi thực sự vẫn chưa hiểu được quan điểm của anh ấy là gì. Ngay khi bạn nói "Tất cả các phương thức phải lấy chính xác một tham số" thì bạn đã thất bại phải không? Ở giai đoạn mà bạn nói, OK, điều này đặt ra một số hạn chế nhân tạo nghiêm trọng đối với khả năng viết phần mềm của tôi, chúng ta hãy bỏ nó ngay bây giờ, ba tháng nữa chúng ta có một cơ sở mã ác mộng hoàn chỉnh để làm việc.

Và bạn biết những gì? Bạn có thể viết một khung ghi nhật ký dựa trên IL dựa trên thuộc tính đơn giản khá dễ dàng với Mono.Cecil . (kiểm tra nó hơi phức tạp hơn một chút, nhưng ...)

Oh và IMO, nếu bạn không sử dụng thuộc tính, thì đó không phải là AOP. Toàn bộ việc thực hiện mã ghi nhật ký nhập / thoát phương thức ở giai đoạn xử lý bài là để nó không gây rối với các tệp mã của bạn, vì vậy bạn không cần phải suy nghĩ về nó khi bạn cấu trúc lại mã của mình; đó sức mạnh của nó

Tất cả Greg đã chứng minh rằng có một mô hình ngu ngốc ngu ngốc.


6
+1 để giữ cho nó ngu ngốc ngu ngốc. Nhắc nhở tôi về câu nói nổi tiếng của Einstein: "làm cho mọi thứ đơn giản nhất có thể, nhưng không đơn giản hơn."
Rei Miyasaka

FWIW, F # có cùng một hạn chế, mỗi phương thức lấy tối đa một đối số.
R0MANARMY

1
let concat (x : string) y = x + y;; concat "Hello, " "World!";;Có vẻ như phải mất hai đối số, tôi còn thiếu gì?

2
@ Miệng - điều thực sự xảy ra là với việc concat "Hello, "bạn thực sự tạo ra một chức năng chỉ cần yxđược xác định trước là một ràng buộc cục bộ là "Xin chào". Nếu chức năng trung gian này có thể được nhìn thấy, nó sẽ trông giống như vậy let concat_x y = "Hello, " + y. Và sau đó, bạn đang gọi concat_x "World!". Cú pháp làm cho nó ít rõ ràng hơn, nhưng điều này cho phép bạn "nướng" các chức năng mới - ví dụ , let printstrln = print "%s\n" ;; printstrln "woof". Ngoài ra, ngay cả khi bạn làm điều gì đó giống như let f(x,y) = x + y, đó là thực sự chỉ là một tuple tranh cãi.
Rei Miyasaka

1
Lần đầu tiên tôi làm bất kỳ chương trình chức năng nào là ở Miranda hồi đại học, tôi sẽ phải xem F #, nghe có vẻ thú vị.

8

Chúa ơi, anh chàng đó là không thể chịu đựng được. Tôi ước tôi chỉ đọc mã trong câu hỏi của bạn thay vì xem cuộc nói chuyện đó.

Tôi không nghĩ rằng tôi đã từng sử dụng phương pháp này nếu nó chỉ vì mục đích sử dụng AOP. Greg nói rằng nó tốt cho các tình huống đơn giản. Đây là những gì tôi sẽ làm trong một tình huống đơn giản:

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

Vâng, tôi đã làm nó, tôi đã hoàn toàn thoát khỏi AOP! Tại sao? Bởi vì bạn không cần AOP trong những tình huống đơn giản .

Từ quan điểm lập trình chức năng, chỉ cho phép một tham số cho mỗi chức năng không thực sự làm tôi sợ. Tuy nhiên, đây thực sự không phải là một thiết kế hoạt động tốt với C # - và đi ngược lại các hạt ngôn ngữ của bạn không KIẾM bất cứ điều gì.

Tôi chỉ sử dụng cách tiếp cận này nếu cần thiết phải tạo một mô hình lệnh để bắt đầu, ví dụ nếu tôi cần một ngăn xếp hoàn tác hoặc nếu tôi đang làm việc với Lệnh WPF .

Nếu không, tôi sẽ chỉ sử dụng một khung hoặc một số phản ánh. PostSharp thậm chí hoạt động trong Silverlight và Compact Framework - vì vậy thứ mà anh gọi là "ma thuật" thực sự không hề kỳ diệu chút nào .

Tôi cũng không đồng ý với việc tránh các khuôn khổ vì mục đích có thể giải thích mọi thứ với đàn em. Nó không làm họ tốt chút nào. Nếu Greg đối xử với đàn em của mình theo cách mà anh ta đề nghị họ được đối xử, như những kẻ ngốc đầu lâu, thì tôi nghi ngờ rằng các nhà phát triển cấp cao của anh ta cũng không tuyệt lắm, vì có lẽ họ đã không có cơ hội học hỏi bất cứ điều gì trong thời gian họ năm học cơ sở.


5

Tôi đã làm một nghiên cứu độc lập ở trường đại học về AOP. Tôi thực sự đã viết một bài báo về cách tiếp cận mô hình AOP với một trình cắm thêm Eclipse. Điều đó thực sự có phần không liên quan tôi cho rằng. Những điểm chính là 1) Tôi còn trẻ và thiếu kinh nghiệm và 2) Tôi đã làm việc với AspectJ. Tôi có thể nói với bạn rằng "ma thuật" của hầu hết các khung AOP không quá phức tạp. Tôi thực sự đã làm việc trên một dự án vào khoảng thời gian đó đang cố gắng thực hiện cách tiếp cận tham số duy nhất bằng cách sử dụng hàm băm. IMO, cách tiếp cận tham số duy nhất thực sự là một khung và nó là xâm lấn. Ngay cả trên bài đăng này, tôi đã dành nhiều thời gian hơn để cố gắng hiểu cách tiếp cận tham số duy nhất hơn là tôi đã xem xét phương pháp khai báo. Tôi sẽ thêm một cảnh báo mà tôi chưa xem phim, vì vậy "phép thuật" của phương pháp này có thể là sử dụng các ứng dụng một phần.

Tôi nghĩ Greg đã trả lời câu hỏi của bạn. Bạn nên chuyển sang cách tiếp cận này khi bạn nghĩ rằng bạn đang ở trong một tình huống mà bạn dành quá nhiều thời gian để giải thích các khung AOP cho các nhà phát triển cơ sở của bạn. IMO, nếu bạn ở trong chiếc thuyền này, có lẽ bạn đang thuê các nhà phát triển cơ sở sai. Tôi không tin AOP yêu cầu một cách tiếp cận khai báo, nhưng đối với tôi, nó rõ ràng hơn và không xâm lấn từ góc độ thiết kế.


+1 cho "Tôi đã dành nhiều thời gian hơn để cố gắng hiểu cách tiếp cận tham số duy nhất so với khi tôi xem lại cách tiếp cận khai báo." Tôi tìm thấy IConsume<T>ví dụ quá phức tạp cho những gì đang được hoàn thành.
Scott Whitlock

4

Trừ khi tôi thiếu thứ gì đó mà mã bạn đã hiển thị là mẫu thiết kế 'chuỗi trách nhiệm', rất tốt nếu bạn cần nối một loạt các hành động trên một đối tượng (chẳng hạn như các lệnh đi qua một loạt các trình xử lý lệnh) tại thời gian chạy.

AOP sử dụng PostSharp là tốt nếu bạn biết tại thời điểm biên dịch hành vi bạn muốn thêm sẽ là gì. Mã của PostSharp được dệt khá nhiều có nghĩa là không có chi phí thời gian chạy bằng 0 và thực sự giữ mã rất sạch (đặc biệt là khi bạn bắt đầu sử dụng những thứ như các khía cạnh phát đa hướng). Tôi không nghĩ cách sử dụng cơ bản của PostSharp là đặc biệt phức tạp để giải thích. Nhược điểm của PostSharp là nó tăng thời gian biên dịch một cách đáng kể.

Tôi sử dụng cả hai kỹ thuật trong mã sản xuất và mặc dù có một số trùng lặp về nơi chúng có thể được áp dụng, tôi nghĩ phần lớn chúng thực sự nhắm vào các kịch bản khác nhau.


4

Về sự thay thế của anh ấy - đã ở đó, làm điều đó. Không có gì có thể so sánh với khả năng đọc của thuộc tính một dòng.

Đưa ra một bài giảng ngắn cho những người mới giải thích cho họ cách mọi thứ hoạt động trong AOP.


4

Những gì Greg mô tả là hoàn toàn hợp lý. Và có vẻ đẹp trong đó là tốt. Khái niệm này được áp dụng trong một mô hình khác với định hướng đối tượng thuần túy. Đó là một cách tiếp cận theo thủ tục hoặc một cách tiếp cận thiết kế theo dòng chảy. Vì vậy, nếu bạn đang làm việc với mã kế thừa, sẽ khá khó để áp dụng khái niệm này vì có thể cần rất nhiều phép tái cấu trúc.

Tôi sẽ cố gắng đưa ra một ví dụ khác. Có thể không hoàn hảo nhưng tôi hy vọng nó làm cho quan điểm rõ ràng hơn.

Vì vậy, chúng tôi có một dịch vụ sản phẩm sử dụng kho lưu trữ (trong trường hợp này chúng tôi sẽ sử dụng sơ khai). Dịch vụ sẽ nhận được một danh sách các sản phẩm.

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 }
        };
    }
}

Dĩ nhiên bạn cũng có thể chuyển giao diện cho dịch vụ.

Tiếp theo chúng tôi muốn hiển thị một danh sách các sản phẩm trong một khung nhìn. Vì vậy, chúng ta cần một giao diện

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

và một lệnh giữ danh sách các sản phẩm

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

và quan điểm

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

Bây giờ chúng ta cần một số mã thực thi tất cả điều này. Điều này chúng ta sẽ làm trong một lớp gọi là Ứng dụng. Phương thức Run () là phương thức tích hợp không chứa hoặc ít nhất là rất ít logic nghiệp vụ. Các phụ thuộc được đưa vào hàm tạo như các phương thức.

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);
    }
}

Cuối cùng chúng tôi soạn ứng dụng theo phương thức chính.

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();
}

Bây giờ, điều thú vị là chúng ta có thể thêm các khía cạnh như ghi nhật ký hoặc xử lý ngoại lệ mà không cần chạm vào mã hiện có và không có khung hoặc chú thích. Để xử lý ngoại lệ, ví dụ: chúng ta chỉ cần thêm một lớp mới:

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);
        }
    }
}

Và sau đó chúng tôi cắm nó lại với nhau trong quá trình sáng tác tại điểm vào của ứng dụng. chúng ta thậm chí không phải chạm vào mã trong lớp Ứng dụng. Chúng tôi chỉ thay thế một dòng:

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

Vì vậy, để tiếp tục: Khi chúng ta có một thiết kế hướng dòng chảy, chúng ta có thể thêm các khía cạnh bằng cách thêm chức năng bên trong một lớp mới. Sau đó, chúng ta phải thay đổi một dòng trong phương thức sáng tác và đó là nó.

Vì vậy, tôi nghĩ rằng một câu trả lời cho câu hỏi của bạn là bạn không thể dễ dàng chuyển đổi từ phương pháp này sang phương pháp khác nhưng bạn phải quyết định loại phương pháp kiến ​​trúc nào bạn sẽ thực hiện trong dự án của mình.

chỉnh sửa: Trên thực tế tôi chỉ nhận ra rằng mẫu ứng dụng một phần được sử dụng với dịch vụ sản phẩm làm cho mọi thứ phức tạp hơn một chút. Chúng ta cần phải bao bọc một lớp khác xung quanh phương thức dịch vụ sản phẩm để có thể thêm các khía cạnh ở đây. Nó có thể là một cái gì đó như thế này:

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();
}

Thành phần sau đó phải được thay đổi như thế này:

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);
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.