Các loại tổng so với đa hình


10

Năm ngoái tôi đã có bước nhảy vọt và học một ngôn ngữ lập trình chức năng (F #) và một trong những điều thú vị hơn mà tôi đã tìm thấy là cách nó ảnh hưởng đến cách tôi thiết kế phần mềm OO. Hai điều tôi thấy mình thiếu nhất trong các ngôn ngữ OO là khớp mẫu và kiểu tổng. Ở mọi nơi tôi nhìn thấy tôi đều thấy những tình huống sẽ được mô hình hóa một cách tầm thường với một liên minh bị phân biệt đối xử, nhưng tôi miễn cưỡng làm ầm ĩ trong một số triển khai OO DU mà cảm thấy không tự nhiên đối với mô hình.

Điều này thường dẫn tôi tạo các loại trung gian để xử lý các ormối quan hệ mà loại tổng sẽ xử lý cho tôi. Nó cũng có vẻ dẫn đến một nhánh tốt. Nếu tôi đọc những người như Misko Hevery , anh ấy gợi ý rằng thiết kế OO tốt có thể giảm thiểu sự phân nhánh thông qua đa hình.

Một trong những điều tôi tránh càng nhiều càng tốt trong mã OO là các loại có nullgiá trị. Rõ ràng ormối quan hệ có thể được mô hình hóa bằng một loại với một nullgiá trị và một nullgiá trị không , nhưng điều này có nghĩa là nullthử nghiệm ở mọi nơi. Có cách nào để mô hình hóa các loại không đồng nhất nhưng liên quan đến logic một cách đa hình? Các chiến lược hoặc mẫu thiết kế sẽ rất hữu ích, hoặc đơn giản là cách suy nghĩ về các loại không đồng nhất và các loại liên quan nói chung trong mô hình OO.


3
"Thiết kế OO tốt có thể giảm thiểu phân nhánh thông qua đa hình" : nó chuyển phân nhánh từ logic nghiệp vụ thực tế sang mã khởi tạo / cấu hình. Lợi ích thường là "khởi tạo và cấu hình" xảy ra ít hơn (như các lần xuất hiện trong mã, không phải về mặt "thực thi") so với việc phân nhánh rõ ràng trong logic nghiệp vụ sẽ là cần thiết. Nhược điểm là không có chỗ cho hoặc tùy thuộc vào loại đối tượng mục tiêu trong logic nghiệp vụ ...
Timothy Truckle

3
Điều này có thể khiến bạn quan tâm (về cơ bản, tác giả mô hình một kiểu tổng dưới dạng phân cấp, với một loạt các phương thức được ghi đè trong các lớp con như một cách để mô hình khớp mẫu); ngoài ra, trong OO, có thể tránh kiểm tra null bằng cách sử dụng Mẫu đối tượng Null (chỉ là một đối tượng không làm gì cho một hoạt động đa hình nhất định).
Filip Milovanović

Các mẫu tổng hợp có thể đáng đọc.
candied_orange

1
Bạn có thể đưa ra một ví dụ về loại điều bạn muốn cải thiện?
JimmyJames

@TimothyTruckle Giải thích tốt, nhưng không phải lúc nào cũng là "khởi tạo / cấu hình". Sự phân nhánh xảy ra khi bạn gọi phương thức, vô hình, nhưng một ngôn ngữ động có thể cho phép bạn thêm các lớp một cách linh hoạt, trong trường hợp đó, sự phân nhánh cũng thay đổi linh hoạt.
Frank Hileman

Câu trả lời:


15

Giống như bạn, tôi muốn rằng các công đoàn bị phân biệt đối xử là phổ biến hơn; tuy nhiên, lý do chúng hữu ích trong hầu hết các ngôn ngữ chức năng là vì chúng cung cấp khớp mẫu đầy đủ và không có điều này, chúng chỉ là cú pháp đẹp: không chỉ khớp mẫu: khớp mẫu đầy đủ , để mã không biên dịch nếu bạn không ' t bao gồm mọi khả năng: đây là những gì mang lại cho bạn sức mạnh.

Cách duy nhất để làm bất cứ điều gì hữu ích với một loại tổng là phân tách nó và phân nhánh tùy thuộc vào loại đó (ví dụ: bằng cách khớp mẫu). Điều tuyệt vời về giao diện là bạn không quan tâm loại gì là gì, bởi vì bạn biết bạn có thể coi nó như một iface: không có logic duy nhất cần thiết cho mỗi loại: không phân nhánh.

Đây không phải là "mã chức năng có nhiều nhánh hơn, mã OO có ít hơn", đây là "" ngôn ngữ chức năng "phù hợp hơn với các miền mà bạn có liên kết - bắt buộc phân nhánh - và" ngôn ngữ OO "phù hợp hơn với mã nơi bạn có thể phơi bày hành vi phổ biến như một giao diện chung - có thể cảm thấy như nó ít phân nhánh hơn ". Sự phân nhánh là một chức năng của thiết kế của bạn và miền. Nói một cách đơn giản, nếu "các loại không đồng nhất nhưng được liên kết logic" của bạn không thể hiển thị một giao diện chung, thì bạn phải phân nhánh / khớp mẫu với chúng. Đây là một vấn đề tên miền / thiết kế.

Điều mà Misko có thể đề cập đến là ý tưởng chung rằng nếu bạn có thể trưng bày các loại của mình như một giao diện chung, thì việc sử dụng các tính năng OO (giao diện / đa hình) sẽ giúp cuộc sống của bạn tốt hơn bằng cách đặt hành vi cụ thể theo kiểu thay vì tiêu thụ mã.

Điều quan trọng là phải nhận ra rằng các giao diện và các hiệp hội là loại đối lập với nhau: một giao diện xác định một số nội dung mà loại phải thực hiện và liên minh xác định một số nội dung mà người tiêu dùng phải xem xét. Nếu bạn thêm một phương thức vào một giao diện, bạn đã thay đổi hợp đồng đó và bây giờ mọi loại đã thực hiện trước đó cần phải được cập nhật. Nếu bạn thêm một loại mới vào một liên minh, bạn đã thay đổi hợp đồng đó và bây giờ mọi mẫu phù hợp với liên minh phải được cập nhật. Chúng lấp đầy các vai trò khác nhau và đôi khi có thể thực hiện một hệ thống 'theo cách nào đó', mà bạn đi cùng là một quyết định thiết kế: vốn dĩ không tốt hơn.

Một lợi ích của việc sử dụng giao diện / đa hình là mã tiêu thụ có thể mở rộng hơn: bạn có thể chuyển qua loại không được xác định tại thời điểm thiết kế, miễn là nó hiển thị giao diện đã thỏa thuận. Mặt khác, với một liên minh tĩnh, bạn có thể khai thác các hành vi không được xem xét tại thời điểm thiết kế bằng cách viết các khớp nối mẫu đầy đủ mới, miễn là chúng tuân theo hợp đồng của liên minh.


Về 'Mẫu đối tượng Null': đây không phải là viên đạn bạc và không thay thế nullséc. Tất cả những gì nó cung cấp một cách để tránh một số kiểm tra 'null' trong đó hành vi 'null' có thể được phơi bày đằng sau một giao diện chung. Nếu bạn không thể phơi bày hành vi 'null' đằng sau giao diện của loại, thì bạn sẽ nghĩ "Tôi thực sự ước mình có thể mô hình hoàn toàn khớp với điều này" và cuối cùng sẽ thực hiện kiểm tra 'phân nhánh'.


4
liên quan đến đoạn áp chót: en.wikipedia.org/wiki/Expression_propet
jk.

"một giao diện xác định một số nội dung mà loại phải triển khai và liên minh xác định một số nội dung mà người tiêu dùng phải xem xét" - bạn không phải nhìn vào các giao diện theo cách đó. Một thành phần có thể xác định một giao diện bắt buộc - những gì một số thành phần khác phải thực hiện; và một giao diện được cung cấp - một giao diện mà một thành phần người tiêu dùng phải xem xét (nghĩa là được lập trình chống lại).
Filip Milovanović

@ FilipMilovanović aye, tôi đã không chính xác ở đó. Tôi đã cố gắng tránh đi vào 'tam giác' phụ thuộc với các giao diện (người tiêu dùng -> giao diện <- người thực hiện / loại) thay vì phụ thuộc 'tuyến tính' với một liên minh (người tiêu dùng -> liên minh -> loại), vì tôi thực sự chỉ cố gắng diễn tả nơi 'quyết định' đang diễn ra (ví dụ: chúng ta sẽ xác định phải làm gì nếu được trình bày với loại này)
VisualMelon

3

Có một cách khá "chuẩn" để mã hóa các loại tổng thành ngôn ngữ hướng đối tượng.

Đây là hai ví dụ:

type Either<'a, 'b> = Left of 'a | Right of 'b

Trong C #, chúng ta có thể biểu hiện điều này như sau:

interface Either<A, B> {
    C Match<C>(Func<A, C> left, Func<B, C> right);
}

class Left<A, B> : Either<A, B> {
    private readonly A a;
    public Left(A a) { this.a = a; }
    public C Match<C>(Func<A, C> left, Func<B, C> right) {
        return left(a);
    }
}

class Right<A, B> : Either<A, B> {
    private readonly B b;
    public Right(B b) { this.b = b; }
    public C Match<C>(Func<A, C> left, Func<B, C> right) {
        return right(b);
    }
}

Một lần nữa:

type List<'a> = Nil | Cons of 'a * List<'a>

C # một lần nữa:

interface List<A> {
    B Match<B>(B nil, Func<A, List<A>, B> cons);
}

class Nil<A> : List<A> {
    public Nil() {}
    public B Match<B>(B nil, Func<A, List<A>, B> cons) {
        return nil;
    }
}

class Cons<A> : List<A> {
    private readonly A head;
    private readonly List<A> tail;
    public Cons(A head, List<A> tail) {
        this.head = head;
        this.tail = tail;
    }
    public B Match<B>(B nil, Func<A, List<A>, B> cons) {
        return cons(head, tail);
    }
}

Việc mã hóa là hoàn toàn cơ học. Mã hóa này tạo ra một kết quả có hầu hết các ưu điểm và nhược điểm giống nhau của các loại dữ liệu đại số. Bạn cũng có thể nhận ra đây là một biến thể của Mẫu khách truy cập. Chúng tôi có thể thu thập các tham số để Matchcùng nhau vào một giao diện mà chúng tôi có thể gọi là Khách truy cập.

Về mặt lợi thế, điều này cung cấp cho bạn một mã hóa nguyên tắc của các loại tổng. (Đó là mã hóa Scott .) Nó cung cấp cho bạn "khớp mẫu" đầy đủ mặc dù chỉ có một "lớp" khớp. Matchvề mặt nào đó, giao diện "hoàn chỉnh" cho các loại này và mọi hoạt động bổ sung mà chúng tôi muốn có thể được xác định theo nghĩa của nó. Nó trình bày một góc nhìn khác về nhiều mẫu OO như Mẫu đối tượng Null và Mẫu trạng thái như tôi đã chỉ ra trong câu trả lời của Ryathal, cũng như Mẫu khách truy cập và Mẫu tổng hợp. Kiểu Option/ Maybegiống như một mẫu đối tượng Null chung. Mẫu tổng hợp gần giống với mã hóa type Tree<'a> = Leaf of 'a | Children of List<Tree<'a>>. Mẫu trạng thái về cơ bản là một bảng mã của một bảng liệt kê.

Về mặt bất lợi, như tôi đã viết, Matchphương thức này đặt ra một số ràng buộc về những lớp con nào có thể được thêm vào một cách có ý nghĩa, đặc biệt nếu chúng ta muốn duy trì Thuộc tính thay thế Liskov. Ví dụ, áp dụng mã hóa này cho loại liệt kê sẽ không cho phép bạn mở rộng bảng liệt kê một cách có ý nghĩa. Nếu bạn muốn mở rộng bảng liệt kê, bạn sẽ phải thay đổi tất cả người gọi và người triển khai ở mọi nơi giống như khi bạn đang sử dụng enumswitch. Điều đó nói rằng, mã hóa này có phần linh hoạt hơn so với bản gốc. Ví dụ: chúng ta có thể thêm một người Appendtriển khai Listchỉ giữ hai danh sách cho chúng ta nối thêm thời gian liên tục. Điều này sẽ hoạt động giống như các danh sách được nối với nhau nhưng sẽ được trình bày theo một cách khác.

Tất nhiên, nhiều vấn đề trong số này phải làm với thực tế Matchlà phần nào (về mặt khái niệm nhưng có chủ ý) gắn liền với các lớp con. Nếu chúng tôi sử dụng các phương thức không cụ thể, chúng tôi sẽ có được các thiết kế OO truyền thống hơn và chúng tôi lấy lại được khả năng mở rộng, nhưng chúng tôi mất "tính hoàn chỉnh" của giao diện và do đó chúng tôi mất khả năng xác định bất kỳ hoạt động nào trên loại này theo điều khoản của giao diện. Như đã đề cập ở nơi khác, đây là một biểu hiện của Vấn đề Biểu hiện .

Có thể cho rằng, các thiết kế như trên có thể được sử dụng một cách có hệ thống để loại bỏ hoàn toàn nhu cầu phân nhánh từng đạt được lý tưởng OO. Ví dụ, Smalltalk sử dụng mẫu này thường bao gồm cho chính Booleans. Nhưng như các cuộc thảo luận trước đây cho thấy, việc "loại bỏ phân nhánh" này là khá ảo tưởng. Chúng tôi vừa thực hiện phân nhánh theo một cách khác, và nó vẫn có nhiều thuộc tính giống nhau.


1

Xử lý null có thể được thực hiện với mẫu đối tượng null . Ý tưởng là tạo một thể hiện của các đối tượng của bạn trả về các giá trị mặc định cho mọi thành viên và có các phương thức không làm gì cả, nhưng cũng không bị lỗi. Điều này không loại bỏ hoàn toàn kiểm tra null, nhưng nó có nghĩa là bạn chỉ cần kiểm tra null khi tạo đối tượng và trả về đối tượng null của bạn.

Mẫu trạng thái là một cách để giảm thiểu phân nhánh và cung cấp một số lợi ích của khớp mẫu. Một lần nữa, nó đẩy logic phân nhánh để tạo đối tượng. Mỗi trạng thái là một triển khai riêng của giao diện cơ sở, vì vậy tất cả các mã tiêu thụ chỉ cần gọi DoStuff () và phương thức thích hợp được gọi. Một số ngôn ngữ cũng đang thêm khớp mẫu như một tính năng, C # là một ví dụ.


(Un?) Trớ trêu thay, đây là cả hai ví dụ về cách "tiêu chuẩn" để mã hóa các loại kết hợp phân biệt đối xử thành OOP.
Derek Elkins rời SE
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.