Kiến trúc / thành phần ứng dụng trong F #


76

Tôi đã làm SOLID trong C # ở mức độ khá cao trong thời gian gần đây và tại một thời điểm nào đó tôi nhận ra rằng về cơ bản ngày nay tôi không làm nhiều việc khác ngoài việc soạn các hàm. Và sau khi tôi bắt đầu nhìn lại F # một lần nữa, tôi nhận ra rằng nó có lẽ sẽ là lựa chọn ngôn ngữ thích hợp hơn nhiều cho phần lớn những gì tôi đang làm bây giờ, vì vậy tôi muốn thử và chuyển một dự án C # trong thế giới thực sang F # như một bằng chứng về khái niệm. Tôi nghĩ rằng tôi có thể rút ra mã thực tế (theo một cách rất không thành ngữ), nhưng tôi không thể tưởng tượng một kiến ​​trúc sẽ trông như thế nào cho phép tôi làm việc theo kiểu linh hoạt tương tự như trong C #.

Ý tôi là tôi có rất nhiều lớp và giao diện nhỏ mà tôi soạn bằng IoC container, và tôi cũng sử dụng nhiều mẫu như Decorator và Composite. Điều này dẫn đến (theo ý kiến ​​của tôi) một kiến ​​trúc tổng thể rất linh hoạt và có thể phát triển cho phép tôi dễ dàng thay thế hoặc mở rộng chức năng tại bất kỳ điểm nào của ứng dụng. Tùy thuộc vào mức độ lớn của thay đổi được yêu cầu, tôi có thể chỉ cần viết một triển khai mới của một giao diện, thay thế nó trong đăng ký IoC là xong. Ngay cả khi thay đổi lớn hơn, tôi có thể thay thế các phần của biểu đồ đối tượng trong khi phần còn lại của ứng dụng chỉ đơn giản là đứng như trước đây.

Bây giờ với F #, tôi không có các lớp và giao diện (tôi biết tôi có thể, nhưng tôi nghĩ đó là điều không cần thiết khi tôi muốn lập trình hàm thực tế), tôi không có hàm khởi tạo và tôi không có IoC hộp đựng. Tôi biết tôi có thể làm một cái gì đó giống như một mẫu Decorator bằng cách sử dụng các hàm bậc cao hơn, nhưng điều đó dường như không mang lại cho tôi sự linh hoạt và khả năng bảo trì giống như các lớp có chèn hàm tạo.

Hãy xem xét các loại C # sau:

public class Dings
{
    public string Lol { get; set; }

    public string Rofl { get; set; }
}

public interface IGetStuff
{
    IEnumerable<Dings> For(Guid id);
}

public class AsdFilteringGetStuff : IGetStuff
{
    private readonly IGetStuff _innerGetStuff;

    public AsdFilteringGetStuff(IGetStuff innerGetStuff)
    {
        this._innerGetStuff = innerGetStuff;
    }

    public IEnumerable<Dings> For(Guid id)
    {
        return this._innerGetStuff.For(id).Where(d => d.Lol == "asd");
    }
}

public class GeneratingGetStuff : IGetStuff
{
    public IEnumerable<Dings> For(Guid id)
    {
        IEnumerable<Dings> dingse;

        // somehow knows how to create correct dingse for the ID

        return dingse;
    }
}

Tôi sẽ nói với container IoC của tôi để giải quyết AsdFilteringGetStuffcho IGetStuffGeneratingGetStuffcho sự phụ thuộc của mình với giao diện đó. Bây giờ nếu tôi cần một bộ lọc khác hoặc xóa bộ lọc hoàn toàn, tôi có thể cần triển khai tương ứng IGetStuffvà sau đó chỉ cần thay đổi đăng ký IoC. Miễn là giao diện vẫn giữ nguyên, tôi không cần phải chạm vào những thứ bên trong ứng dụng. OCP và LSP, được kích hoạt bởi DIP.

Bây giờ tôi phải làm gì trong F #?

type Dings (lol, rofl) =
    member x.Lol = lol
    member x.Rofl = rofl

let GenerateDingse id =
    // create list

let AsdFilteredDingse id =
    GenerateDingse id |> List.filter (fun x -> x.Lol = "asd")

Tôi thích mã này ít hơn bao nhiêu, nhưng tôi mất tính linh hoạt. Có, tôi có thể gọi AsdFilteredDingsehoặc GenerateDingseở cùng một nơi, vì các loại đều giống nhau - nhưng làm cách nào để tôi quyết định gọi cái nào mà không cần mã hóa khó tại trang web cuộc gọi? Ngoài ra, mặc dù hai chức năng này có thể hoán đổi cho nhau, nhưng bây giờ tôi không thể thay thế chức năng máy phát điện bên trong AsdFilteredDingsemà không thay đổi chức năng này. Điều này không tốt cho lắm.

Lần thử tiếp theo:

let GenerateDingse id =
    // create list

let AsdFilteredDingse (generator : System.Guid -> Dings list) id =
    generator id |> List.filter (fun x -> x.Lol = "asd")

Bây giờ tôi có khả năng kết hợp bằng cách biến AsdFilteredDingse thành một hàm bậc cao hơn, nhưng hai hàm không thể hoán đổi cho nhau nữa. Suy nghĩ thứ hai, họ có lẽ không nên như vậy.

Tôi có thể làm gì khác? Tôi có thể bắt chước khái niệm "gốc sáng tác" từ C # SOLID của tôi trong tệp cuối cùng của dự án F #. Hầu hết các tệp chỉ là tập hợp các chức năng, sau đó tôi có một số loại "sổ đăng ký", thay thế vùng chứa IoC và cuối cùng có một chức năng mà tôi gọi để thực sự chạy ứng dụng và sử dụng các chức năng từ "sổ đăng ký". Trong "sổ đăng ký", tôi biết tôi cần một chức năng thuộc loại (Hướng dẫn -> Danh sách Dings), mà tôi sẽ gọi GetDingseForId. Đây là cái tôi gọi, không bao giờ là các hàm riêng lẻ được định nghĩa trước đó.

Đối với người trang trí, định nghĩa sẽ là

let GetDingseForId id = AsdFilteredDingse GenerateDingse

Để xóa bộ lọc, tôi sẽ thay đổi bộ lọc đó thành

let GetDingseForId id = GenerateDingse

Nhược điểm (?) Của điều này là tất cả các hàm sử dụng các hàm khác hợp lý sẽ phải là các hàm bậc cao hơn và "sổ đăng ký" của tôi sẽ phải ánh xạ tất cả các hàm mà tôi sử dụng, bởi vì các hàm thực tế được xác định trước đó không thể gọi bất kỳ hàm nào các chức năng được xác định sau đó, đặc biệt không phải các chức năng từ "sổ đăng ký". Tôi cũng có thể gặp phải các vấn đề phụ thuộc vòng tròn với ánh xạ "sổ đăng ký".

co phải vai Điêu nay không co y nghia gi? Làm thế nào để bạn thực sự xây dựng một ứng dụng F # để có thể bảo trì và phát triển được (chưa kể có thể kiểm tra được)?

Câu trả lời:


59

Điều này rất dễ dàng khi bạn nhận ra rằng Object-Oriented Constructor Injection tương ứng rất chặt chẽ với Ứng dụng hàm một phần chức năng .

Đầu tiên, tôi viết Dingsdưới dạng bản ghi:

type Dings = { Lol : string; Rofl : string }

Trong F #, IGetStuffgiao diện có thể được thu gọn thành một chức năng duy nhất với chữ ký

Guid -> seq<Dings>

Một ứng dụng khách sử dụng hàm này sẽ coi nó như một tham số:

let Client getStuff =
    getStuff(Guid("055E7FF1-2919-4246-876E-1DA71980BE9C")) |> Seq.toList

Chữ ký cho Clienthàm là:

(Guid -> #seq<'b>) -> 'b list

Như bạn có thể thấy, nó lấy một chức năng của chữ ký đích làm đầu vào và trả về một danh sách.

Máy phát điện

Hàm tạo rất dễ viết:

let GenerateDingse id =
    seq {
        yield { Lol = "Ha!"; Rofl = "Ha ha ha!" }
        yield { Lol = "Ho!"; Rofl = "Ho ho ho!" }
        yield { Lol = "asd"; Rofl = "ASD" } }

Các GenerateDingsechức năng có chữ ký này:

'a -> seq<Dings>

Đây là thực tế hơn generic hơn Guid -> seq<Dings>, nhưng đó không phải là một vấn đề. Nếu bạn chỉ muốn soạn thảo Clientvới GenerateDingse, bạn có thể chỉ cần sử dụng nó như sau:

let result = Client GenerateDingse

Mà sẽ trả về cả ba Dinggiá trị từ GenerateDingse.

Người trang trí

Decorator ban đầu khó hơn một chút, nhưng không nhiều. Nói chung, thay vì thêm kiểu Trang trí (bên trong) làm đối số của hàm tạo, bạn chỉ cần thêm kiểu này dưới dạng giá trị tham số cho một hàm:

let AdsFilteredDingse id s = s |> Seq.filter (fun d -> d.Lol = "asd")

Hàm này có chữ ký này:

'a -> seq<Dings> -> seq<Dings>

Đó không hoàn toàn là những gì chúng tôi muốn, nhưng thật dễ dàng để soạn nó với GenerateDingse:

let composed id = GenerateDingse id |> AdsFilteredDingse id

Các composedchức năng có chữ ký

'a -> seq<Dings>

Chỉ những gì chúng tôi đang tìm kiếm!

Bây giờ bạn có thể sử dụng Clientvới composednhư thế này:

let result = Client composed

mà sẽ chỉ trở lại [{Lol = "asd"; Rofl = "ASD";}].

Bạn không cần phải xác định composedchức năng trước; bạn cũng có thể soạn nó ngay tại chỗ:

let result = Client (fun id -> GenerateDingse id |> AdsFilteredDingse id)

Điều này cũng trở lại [{Lol = "asd"; Rofl = "ASD";}].

Trang trí thay thế

Ví dụ trước hoạt động tốt, nhưng không thực sự Trang trí một chức năng tương tự. Đây là một giải pháp thay thế:

let AdsFilteredDingse id f = f id |> Seq.filter (fun d -> d.Lol = "asd")

Hàm này có chữ ký:

'a -> ('a -> #seq<Dings>) -> seq<Dings>

Như bạn có thể thấy, fđối số là một hàm khác có cùng chữ ký, vì vậy nó gần giống với mẫu Decorator hơn. Bạn có thể soạn nó như thế này:

let composed id = GenerateDingse |> AdsFilteredDingse id

Một lần nữa, bạn có thể sử dụng Clientvới composednhư thế này:

let result = Client composed

hoặc nội dòng như thế này:

let result = Client (fun id -> GenerateDingse |> AdsFilteredDingse id)

Để biết thêm các ví dụ và nguyên tắc để soạn toàn bộ ứng dụng với F #, hãy xem khóa học trực tuyến của tôi về Kiến trúc chức năng với F # .

Để biết thêm về Nguyên tắc hướng đối tượng và cách chúng ánh xạ đến Lập trình chức năng, hãy xem bài đăng trên blog của tôi về các nguyên tắc SOLID và cách chúng áp dụng cho FP .


5
Cảm ơn Mark cho câu trả lời sâu rộng. Tuy nhiên, điều tôi vẫn chưa rõ là nơi soạn thảo trong một ứng dụng có nhiều chức năng tầm thường. Trong thời gian đó, tôi đã chuyển một ứng dụng C # nhỏ sang F # và đăng nó lên đây để xem xét; Tôi đã triển khai "sổ đăng ký" được đề cập ở đó dưới dạng một Compositionmô-đun, về cơ bản là một gốc cấu thành "DI người nghèo". Đó có phải là một cách khả thi để đi?
TeaDrivenDev

@TeaDrivenDev Như bạn có thể nhận thấy, tôi đã cập nhật bài đăng bằng cách thêm liên kết vào bài đăng blog đào sâu hơn một chút. Khi nói đến thành phần, một trong những điều thú vị về F # là trình biên dịch ngăn chặn các tham chiếu vòng tròn, do đó, thành phần và phân tách an toàn hơn rất nhiều so với trong C # . Đoạn mã mà bạn đã liên kết để soạn thảo ở phần cuối, vì vậy nó có vẻ tốt.
Mark Seemann

Định nghĩa thay thế của các hàm có thể là: let AdsFilteredDingse = Seq.filter (fun d -> d.Lol = "asd")let composed = GenerateDingse >> AdsFilteredDingse. Bằng cách này, bạn có thể sử dụng toán tử cấu thành hàm và bạn có thể loại bỏ id khỏi AdsFilteredDingse. Nhưng một lần nữa, như Mark đã đề cập, nó không thực sự là một nhà trang trí theo nghĩa OO ban đầu của từ này.
Simon Stender Boisen

Tôi đã thử điều đó, nhưng nó mang lại cho tôi lỗi trình biên dịch, mặc dù chữ ký của có composedvẻ tốt: "error FS0030: Giới hạn giá trị. Giá trị 'soạn' được suy ra là có kiểu chung val gồm: ('_a -> seq < Dings>) Hãy đưa ra các đối số cho 'sáng tác' rõ ràng hoặc, nếu bạn không có ý định cho nó là chung chung, hãy thêm chú thích kiểu ".
Mark Seemann
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.