Nguyên tắc phân chia giao diện: Phải làm gì nếu giao diện có sự chồng chéo đáng kể?


9

Từ phát triển phần mềm, nguyên tắc, mô hình và thực tiễn của Agile: Pearson Phiên bản quốc tế mới :

Đôi khi, các phương thức được gọi bởi các nhóm khách hàng khác nhau sẽ chồng chéo lên nhau. Nếu sự trùng lặp là nhỏ, thì các giao diện cho các nhóm sẽ được tách biệt. Các chức năng phổ biến nên được khai báo trong tất cả các giao diện chồng chéo. Lớp máy chủ sẽ kế thừa các chức năng chung từ mỗi giao diện đó, nhưng nó sẽ chỉ thực hiện chúng một lần.

Chú Bob, nói về trường hợp khi có sự chồng chéo nhỏ.

Chúng ta nên làm gì nếu có sự chồng chéo đáng kể?

Nói rằng chúng ta có

Class UiInterface1;
Class UiInterface2;
Class UiInterface3;

Class UiIterface : public UiInterface1, public UiInterface2, public UiInterface3{};

Chúng ta nên làm gì nếu có sự chồng chéo đáng kể giữa UiInterface1UiInterface2?


Khi tôi gặp một giao diện chồng chéo rất nhiều, tôi tạo một giao diện cha, nhóm các phương thức phổ biến và sau đó kế thừa từ giao diện chung này để tạo ra các chuyên môn. NHƯNG! Nếu bạn không bao giờ muốn bất kỳ ai sử dụng giao diện chung mà không có chuyên môn, thì bạn thực sự cần phải sao chép mã, bởi vì nếu bạn giới thiệu giao diện chung, mọi người có thể sử dụng giao diện chung đó.
Andy

Câu hỏi hơi mơ hồ với tôi, người ta có thể trả lời bằng nhiều giải pháp khác nhau tùy từng trường hợp. Tại sao sự chồng chéo phát triển?
Arthur Havlicek

Câu trả lời:


1

Vật đúc

Điều này gần như chắc chắn sẽ là một tiếp tuyến hoàn chỉnh cho cách tiếp cận của cuốn sách được trích dẫn, nhưng một cách để phù hợp hơn với ISP là nắm lấy một tư duy đúc tại một khu vực trung tâm của cơ sở mã của bạn bằng cách sử dụng QueryInterfacephương pháp COM.

Rất nhiều cám dỗ để thiết kế các giao diện chồng chéo trong bối cảnh giao diện thuần túy thường xuất phát từ mong muốn làm cho các giao diện "tự túc" hơn là thực hiện một trách nhiệm chính xác, giống như bắn tỉa.

Ví dụ, có vẻ kỳ lạ khi thiết kế các chức năng máy khách như thế này:

// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `position` and `parenting` parameters should point to the 
// same object.
Vec2i abs_position(IPosition* position, IParenting* parenting)
{
     const Vec2i xy = position->xy();
     auto parent = parenting->parent();
     if (parent)
     {
         // If the entity has a parent, return the sum of the
         // parent position and the entity's local position.
         return xy + abs_position(dynamic_cast<IPosition*>(parent),
                                  dynamic_cast<IParenting*>(parent));
     }
     return xy;
}

... cũng như khá xấu xí / nguy hiểm, vì chúng tôi đã rò rỉ trách nhiệm thực hiện việc truyền dễ bị lỗi sang mã máy khách bằng cách sử dụng các giao diện này và / hoặc truyền cùng một đối tượng làm đối số nhiều lần cho nhiều tham số giống nhau chức năng. Vì vậy, cuối cùng chúng tôi thường muốn thiết kế một giao diện loãng hơn, hợp nhất các mối quan tâm của IParentingIPositionở một nơi, giống như IGuiElementhoặc một cái gì đó trở nên dễ bị chồng chéo với các mối quan tâm của các giao diện trực giao cũng sẽ bị cám dỗ để có nhiều chức năng thành viên hơn cùng một lý do "tự túc".

Trộn trách nhiệm so với đúc

Khi thiết kế các giao diện với một trách nhiệm cực kỳ đơn giản, cực kỳ đơn giản, sự cám dỗ thường sẽ là chấp nhận một số giao diện bị bỏ qua hoặc hợp nhất để thực hiện nhiều trách nhiệm (và do đó bước đi trên cả ISP và SRP).

Bằng cách sử dụng cách tiếp cận theo kiểu COM (chỉ là QueryInterfacemột phần), chúng tôi chơi theo cách tiếp cận hạ thấp nhưng hợp nhất việc truyền đến một vị trí trung tâm trong cơ sở mã và có thể làm một cái gì đó giống như sau:

// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `obj` should implement `IPosition` and optionally `IParenting`.
Vec2i abs_position(Object* obj)
{
     // `Object::query_interface` returns nullptr if the interface is
     // not provided by the entity. `Object` is an abstract base class
     // inherited by all entities using this interface query system.
     IPosition* position = obj->query_interface<IPosition>();
     assert(position && "obj does not implement IPosition!");
     const Vec2i xy = position->xy();

     IParenting* parenting = obj->query_interface<IParenting>();
     if (parenting && parenting->parent()->query_interface<IPosition>())
     {
         // If the entity implements IParenting and has a parent, 
         // return the sum of the parent position and the entity's 
         // local position.
         return xy + abs_position(parenting->parent());
     }
     return xy;
}

... Tất nhiên là hy vọng với các trình bao bọc an toàn kiểu và tất cả những gì bạn có thể xây dựng tập trung để có được thứ gì đó an toàn hơn so với con trỏ thô.

Với điều này, sự cám dỗ để thiết kế các giao diện chồng chéo thường được giảm thiểu đến mức tối thiểu. Nó cho phép bạn thiết kế các giao diện với các trách nhiệm rất đơn lẻ (đôi khi chỉ có một chức năng thành viên bên trong) mà bạn có thể trộn và khớp với tất cả những gì bạn thích mà không phải lo lắng về ISP và có được sự linh hoạt của việc gõ giả giả trong thời gian chạy trong C ++ (mặc dù tất nhiên là với sự đánh đổi các hình phạt thời gian chạy để truy vấn các đối tượng để xem liệu chúng có hỗ trợ một giao diện cụ thể không). Phần thời gian chạy có thể quan trọng trong một cài đặt với bộ phát triển phần mềm nơi các chức năng sẽ không có thông tin về thời gian biên dịch của các plugin trước khi thực hiện các giao diện này.

Mẫu

Nếu các mẫu là một khả năng (chúng ta có thông tin về thời gian biên dịch cần thiết mà không bị mất theo thời gian chúng ta nắm giữ một đối tượng, tức là), thì chúng ta chỉ cần làm điều này:

// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `obj` should have `position` and `parent` methods.
template <class Entity>
Vec2i abs_position(Entity& obj)
{
     const Vec2i xy = obj.xy();
     if (obj.parent())
     {
         // If the entity has a parent, return the sum of the parent 
         // position and the entity's local position.
         return xy + abs_position(obj.parent());
     }
     return xy;
}

... tất nhiên trong trường hợp như vậy, parentphương thức sẽ phải trả về cùng Entityloại, trong trường hợp đó có lẽ chúng ta muốn tránh các giao diện hoàn toàn (vì chúng thường muốn mất thông tin kiểu để làm việc với các con trỏ cơ sở).

Hệ thống thành phần

Nếu bạn bắt đầu theo đuổi cách tiếp cận kiểu COM hơn từ quan điểm linh hoạt hoặc hiệu suất, bạn sẽ thường kết thúc với một hệ thống thành phần thực thể tương tự như những gì công cụ trò chơi áp dụng trong ngành. Vào thời điểm đó, bạn sẽ hoàn toàn vuông góc với nhiều cách tiếp cận hướng đối tượng, nhưng ECS ​​có thể được áp dụng cho thiết kế GUI (một nơi tôi đã dự tính sử dụng ECS ​​bên ngoài tiêu điểm hướng cảnh, nhưng đã xem xét nó quá muộn giải quyết theo cách tiếp cận kiểu COM để thử ở đó).

Lưu ý rằng giải pháp kiểu COM này hoàn toàn nằm ngoài khả năng của các thiết kế bộ công cụ GUI và ECS thậm chí còn nhiều hơn, do đó, đây không phải là thứ sẽ được hỗ trợ bởi nhiều tài nguyên. Tuy nhiên, nó chắc chắn sẽ cho phép bạn giảm thiểu những cám dỗ để thiết kế giao diện có trách nhiệm chồng chéo đến mức tối thiểu, thường khiến nó không đáng lo ngại.

Cách tiếp cận thực dụng

Tất nhiên, giải pháp thay thế là thư giãn bảo vệ bạn một chút hoặc thiết kế giao diện ở mức chi tiết và sau đó bắt đầu kế thừa chúng để tạo giao diện thô hơn mà bạn sử dụng, giống như IPositionPlusParentingxuất phát từ cả hai IPositionIParenting(hy vọng với một cái tên tốt hơn thế). Với các giao diện thuần túy, nó không nên vi phạm ISP nhiều như các cách tiếp cận phân cấp sâu nguyên khối thường được áp dụng (Qt, MFC, v.v., trong đó tài liệu thường cảm thấy cần phải che giấu các thành viên không liên quan với mức độ vi phạm quá mức của ISP với các loại đó của các thiết kế), vì vậy một cách tiếp cận thực dụng có thể chỉ đơn giản là chấp nhận một số chồng chéo ở đây và đó. Tuy nhiên, cách tiếp cận kiểu COM này tránh được nhu cầu tạo giao diện hợp nhất cho mọi kết hợp bạn sẽ sử dụng. Mối quan tâm "tự cung tự cấp" đã được loại bỏ hoàn toàn trong những trường hợp như vậy và điều đó thường sẽ loại bỏ nguồn cám dỗ cuối cùng để thiết kế các giao diện có trách nhiệm chồng chéo muốn chống lại cả SRP và ISP.


11

Đây là một cuộc gọi phán xét mà bạn phải thực hiện, trên cơ sở từng trường hợp.

Trước hết, hãy nhớ rằng các nguyên tắc RẮN chỉ là ... nguyên tắc. Chúng không phải là quy tắc. Chúng không phải là viên đạn bạc. Họ chỉ là nguyên tắc. Đó không phải là để lấy đi tầm quan trọng của họ, bạn nên luôn luôn nghiêng về phía họ. Nhưng thứ hai họ giới thiệu một mức độ đau đớn, bạn nên bỏ chúng cho đến khi bạn cần chúng.

Với ý nghĩ đó, hãy nghĩ về lý do tại sao bạn tách ra các giao diện của bạn ở nơi đầu tiên. Ý tưởng của một giao diện là nói "Nếu mã tiêu thụ này yêu cầu một bộ phương thức được triển khai trên lớp đang được sử dụng, tôi cần đặt hợp đồng về việc triển khai: Nếu bạn cung cấp cho tôi một đối tượng với giao diện này, tôi có thể làm việc với nó."

Mục đích của ISP là nói "Nếu hợp đồng tôi yêu cầu chỉ là một tập hợp con của giao diện hiện có, tôi không nên thực thi giao diện hiện có trên bất kỳ lớp nào trong tương lai có thể được truyền cho phương thức của tôi."

Hãy xem xét các mã sau đây:

public interface A
{
    void X();
    void Y();
}

public class Foo
{
     public void ConsumeX(A a)
     {
         a.X();
     }
}

Bây giờ chúng ta có một tình huống, nếu chúng ta muốn chuyển một đối tượng mới cho ConsumeX, nó phải thực hiện X () và Y () để phù hợp với hợp đồng.

Vì vậy, chúng ta nên thay đổi mã, ngay bây giờ, để trông giống như ví dụ tiếp theo?

public interface A
{
    void X();
    void Y();
}

public interface B
{
    void X();
}

public class Foo
{
     public void ConsumeX(B b)
     {
         b.X();
     }
}

ISP đề nghị chúng ta nên, vì vậy chúng ta nên nghiêng về quyết định đó. Nhưng, không có ngữ cảnh, thật khó để chắc chắn. Có khả năng chúng tôi sẽ mở rộng A và B không? Có khả năng là họ sẽ mở rộng độc lập? Có khả năng B sẽ thực hiện các phương thức mà A không yêu cầu không? (Nếu không, chúng ta có thể tạo A xuất phát từ B.)

Đây là lời kêu gọi bạn phải đưa ra. Và, nếu bạn thực sự không có đủ thông tin để thực hiện cuộc gọi đó, có lẽ bạn nên chọn tùy chọn đơn giản nhất, đó có thể là mã đầu tiên.

Tại sao? Bởi vì nó dễ dàng thay đổi suy nghĩ của bạn sau này. Khi bạn cần lớp mới đó, chỉ cần tạo một giao diện mới và thực hiện cả trong lớp cũ của bạn.


1
"Trước hết, hãy nhớ rằng các nguyên tắc RẮN chỉ là ... nguyên tắc. Chúng không phải là quy tắc. Chúng không phải là viên đạn bạc. Chúng chỉ là nguyên tắc. Điều đó không làm mất đi tầm quan trọng của chúng, bạn nên luôn luôn dựa vào theo hướng họ. Nhưng lần thứ hai họ giới thiệu một mức độ đau đớn, bạn nên bỏ họ cho đến khi bạn cần họ. ". Điều này nên ở trang đầu tiên của mỗi cuốn sách mẫu / nguyên tắc thiết kế. Một nó sẽ xuất hiện cứ sau 50 trang như một lời nhắc nhở.
Christian Rodriguez
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.