Bạn có tận dụng những lợi ích của nguyên tắc đóng mở không?


11

Nguyên tắc đóng mở (OCP) nói rằng một đối tượng nên được mở để mở rộng nhưng đóng để sửa đổi. Tôi tin rằng tôi hiểu nó và sử dụng nó cùng với SRP để tạo các lớp chỉ làm một việc. Và, tôi cố gắng tạo ra nhiều phương thức nhỏ để có thể trích xuất tất cả các điều khiển hành vi thành các phương thức có thể được mở rộng hoặc ghi đè trong một số lớp con. Vì vậy, tôi kết thúc với các lớp có nhiều điểm mở rộng, có thể thông qua: tiêm phụ thuộc và thành phần, sự kiện, ủy quyền, v.v.

Hãy xem xét một lớp đơn giản, có thể mở rộng sau đây:

class PaycheckCalculator {
    // ...
    protected decimal GetOvertimeFactor() { return 2.0M; }
}

Bây giờ, ví dụ, OvertimeFactorthay đổi thành 1,5. Vì lớp trên được thiết kế để mở rộng, tôi có thể dễ dàng phân lớp và trả về một lớp khác OvertimeFactor.

Nhưng ... mặc dù lớp được thiết kế để mở rộng và tuân thủ OCP, tôi sẽ sửa đổi phương thức duy nhất được đề cập, thay vì phân lớp và ghi đè phương thức đang đề cập và sau đó nối lại các đối tượng của tôi trong bộ chứa IoC của tôi.

Kết quả là tôi đã vi phạm một phần những gì OCP cố gắng thực hiện. Cảm giác như tôi chỉ lười biếng vì ở trên dễ hơn một chút. Tôi có hiểu nhầm OCP không? Tôi có nên thực sự làm một cái gì đó khác nhau? Bạn có tận dụng các lợi ích của OCP khác nhau không?

Cập nhật : dựa trên các câu trả lời có vẻ như ví dụ giả định này là một ví dụ kém vì một số lý do khác nhau. Mục đích chính của ví dụ là chứng minh rằng lớp được thiết kế để được mở rộng bằng cách cung cấp các phương thức mà khi bị ghi đè sẽ thay đổi hành vi của các phương thức công khai mà không cần thay đổi mã nội bộ hoặc mã riêng. Tuy nhiên, tôi chắc chắn đã hiểu nhầm OCP.

Câu trả lời:


9

Nếu bạn đang sửa đổi lớp cơ sở thì nó không thực sự đóng là nó!

Hãy nghĩ về tình huống mà bạn đã phát hành thư viện ra thế giới. Nếu bạn đi và thay đổi hành vi của lớp cơ sở bằng cách sửa đổi hệ số làm thêm thành 1,5 thì bạn đã vi phạm tất cả những người sử dụng mã của bạn giả sử rằng lớp đã bị đóng.

Thực sự để làm cho lớp đóng nhưng mở, bạn nên truy xuất hệ số làm thêm từ một nguồn thay thế (có thể là tệp cấu hình) hoặc chứng minh một phương thức ảo có thể bị ghi đè?

Nếu lớp học đã thực sự đóng thì sau khi thay đổi của bạn, không có trường hợp kiểm thử nào thất bại (giả sử bạn có bảo hiểm 100% với tất cả các trường hợp kiểm tra của bạn) và tôi sẽ cho rằng có trường hợp kiểm tra kiểm tra GetOvertimeFactor() == 2.0M.

Đừng qua kỹ sư

Nhưng đừng lấy nguyên tắc đóng mở này để đưa ra kết luận hợp lý và có mọi thứ có thể cấu hình được ngay từ đầu (đó là về kỹ thuật). Chỉ xác định các bit bạn hiện cần.

Nguyên tắc khép kín không ngăn cản bạn tái thiết kế đối tượng. Nó chỉ ngăn cản bạn thay đổi giao diện công cộng hiện được xác định thành đối tượng của bạn ( các thành viên được bảo vệ là một phần của giao diện công cộng). Bạn vẫn có thể thêm nhiều chức năng miễn là chức năng cũ không bị hỏng.


"Nguyên tắc khép kín không ngăn cản bạn tái thiết kế đối tượng." Trên thực tế, nó làm . Nếu bạn đọc cuốn sách mà Nguyên tắc đóng mở được đề xuất lần đầu tiên hoặc bài viết giới thiệu từ viết tắt "OCP", bạn sẽ thấy nó nói rằng "Không ai được phép thay đổi mã nguồn cho nó" (ngoại trừ lỗi sửa lỗi).
Rogério

@ Rogério: Điều đó có thể đúng (trở lại năm 1988). Nhưng định nghĩa hiện tại (được phổ biến vào năm 1990 khi OO trở nên phổ biến) Tất cả là về việc duy trì một giao diện công cộng nhất quán. During the 1990s, the open/closed principle became popularly redefined to refer to the use of abstracted interfaces, where the implementations can be changed and multiple implementations could be created and polymorphically substituted for each other. vi.wikipedia.org/wiki/Open/closes_principl
Martin York

Cảm ơn đã tham khảo Wikipedia. Nhưng tôi không chắc định nghĩa "hiện tại" thực sự khác biệt, vì nó vẫn dựa vào sự kế thừa kiểu (lớp hoặc giao diện). Và câu trích dẫn "không thay đổi mã nguồn" mà tôi đã đề cập đến từ bài báo OCP 1996 của Robert Martin (được cho là) ​​phù hợp với "định nghĩa hiện tại". Cá nhân, tôi nghĩ rằng Nguyên tắc đóng mở bây giờ sẽ bị lãng quên, nếu Martin không cho nó một từ viết tắt, rõ ràng, có rất nhiều giá trị tiếp thị. Nguyên tắc này đã lỗi thời và có hại, IMO.
Rogério

3

Vì vậy, Nguyên tắc đóng mở là một gotcha ... đặc biệt nếu bạn cố gắng áp dụng nó cùng lúc với YAGNI . Làm thế nào để tôi tuân thủ cả hai cùng một lúc? Áp dụng quy tắc ba . Lần đầu tiên bạn thực hiện một thay đổi, hãy thực hiện nó trực tiếp. Và lần thứ hai cũng vậy. Lần thứ ba, đã đến lúc trừu tượng hóa sự thay đổi đó.

Một cách tiếp cận khác là "đánh lừa tôi một lần ...", khi bạn phải thực hiện thay đổi, hãy áp dụng OCP để bảo vệ chống lại sự thay đổi đó trong tương lai . Tôi gần như sẽ đi xa hơn để đề xuất rằng thay đổi tỷ lệ làm thêm giờ là một câu chuyện mới. "Là một quản trị viên tiền lương, tôi muốn thay đổi tỷ lệ làm thêm giờ để tôi có thể tuân thủ luật lao động hiện hành". Bây giờ bạn có một giao diện người dùng mới để thay đổi tốc độ làm thêm giờ, một cách để lưu trữ nó và GetOvertimeFactor () chỉ cần hỏi kho lưu trữ của nó về tỷ lệ làm thêm giờ là gì.


2

Trong ví dụ bạn đã đăng, hệ số làm thêm giờ phải là biến hoặc hằng. * (Ví dụ Java)

class PaycheckCalculator {
   float overtimeFactor;

   protected float setOvertimeFactor(float overtimeFactor) {
      this.overtimeFactor = overtimeFactor;
   }

   protected float getOvertimeFactor() {
      return overtimeFactor;
   }
}

HOẶC LÀ

class PaycheckCalculator {
   public static final float OVERTIME_FACTOR = 1.5f;
}

Sau đó, khi bạn mở rộng lớp, đặt hoặc ghi đè hệ số. "Số ma thuật" chỉ nên xuất hiện một lần. Đây là nhiều hơn theo kiểu OCP và DRY (Đừng lặp lại chính mình), vì không cần thiết phải tạo một lớp hoàn toàn mới cho một yếu tố khác nếu sử dụng phương thức đầu tiên và chỉ phải thay đổi hằng số trong một thành ngữ nơi thứ hai.

Tôi sẽ sử dụng cái đầu tiên trong trường hợp sẽ có nhiều loại máy tính, mỗi loại cần các giá trị không đổi khác nhau. Một ví dụ sẽ là mẫu Chuỗi trách nhiệm, thường được triển khai bằng các kiểu kế thừa. Một đối tượng chỉ có thể nhìn thấy giao diện (tức là getOvertimeFactor()) sử dụng nó để lấy tất cả thông tin cần thiết, trong khi các kiểu con lo lắng về thông tin thực tế cần cung cấp.

Thứ hai là hữu ích trong trường hợp hằng số không có khả năng thay đổi, nhưng được sử dụng ở nhiều vị trí. Có một hằng số để thay đổi (trong trường hợp không thể xảy ra) sẽ dễ dàng hơn nhiều so với việc đặt nó ở mọi nơi hoặc lấy nó từ một tệp thuộc tính.

Nguyên tắc đóng mở là một cuộc gọi để không sửa đổi đối tượng hiện có hơn là một sự thận trọng để giữ cho giao diện không thay đổi. Nếu bạn cần một số hành vi hơi khác với một lớp hoặc thêm chức năng cho một trường hợp cụ thể, hãy mở rộng và ghi đè. Nhưng nếu các yêu cầu cho chính lớp đó thay đổi (như thay đổi yếu tố), bạn cần thay đổi lớp. Không có điểm nào trong một hệ thống phân cấp lớp lớn, hầu hết chúng không bao giờ được sử dụng.


Đây là thay đổi dữ liệu, không phải thay đổi mã. Tỷ lệ làm thêm giờ không nên được mã hóa cứng.
Jim C

Bạn dường như có Get và Set của bạn ngược lại.
Mason Wheeler

Rất tiếc! nên thử nghiệm ...
Michael K

2

Tôi thực sự không thấy ví dụ của bạn là một đại diện tuyệt vời của OCP. Tôi nghĩ những gì quy tắc thực sự có nghĩa là này:

Khi bạn muốn thêm một tính năng, bạn chỉ cần thêm một lớp và bạn không cần phải sửa đổi bất kỳ lớp nào khác (nhưng có thể là tệp cấu hình).

Một thực hiện kém dưới đây. Mỗi khi bạn thêm một trò chơi, bạn sẽ cần sửa đổi lớp GamePlayer.

class GamePlayer
{
   public void PlayGame(string game)
   {
      switch(game)
      {
          case "Poker":
              PlayPoker();
              break;

          case "Gin": 
              PlayGin();
              break;

          ...
      }
   }

   ...
}

Lớp GamePlayer không bao giờ cần phải sửa đổi

class GamePlayer
{
    ...

    public void PlayGame(string game)
    {
        Game g = GameFactory.GetByName(game); 
        g.Play();   
    }

    ...
}

Bây giờ giả sử GameFactory của tôi cũng tuân thủ OCP, khi tôi muốn thêm một trò chơi khác, tôi chỉ cần xây dựng một lớp mới kế thừa từ Gamelớp và mọi thứ sẽ hoạt động.

Tất cả các lớp quá thường xuyên như lớp đầu tiên được xây dựng sau nhiều năm "mở rộng" và không bao giờ được cấu trúc lại chính xác từ phiên bản gốc (hoặc tệ hơn, những gì nên là nhiều lớp vẫn là một lớp lớn).

Ví dụ bạn cung cấp là OCP-ish. Theo tôi, cách chính xác để xử lý thay đổi tỷ lệ làm thêm giờ sẽ là trong cơ sở dữ liệu với tỷ lệ lịch sử được giữ để dữ liệu có thể được xử lý lại. Mã vẫn phải được đóng để sửa đổi bởi vì nó sẽ luôn tải giá trị phù hợp từ tra cứu.

Như một ví dụ trong thế giới thực, tôi đã sử dụng một biến thể của ví dụ của mình và Nguyên tắc đóng mở thực sự tỏa sáng. Chức năng thực sự dễ dàng để thêm bởi vì tôi chỉ cần xuất phát từ một lớp cơ sở trừu tượng và "nhà máy" của tôi tự động lấy nó và "người chơi" không quan tâm đến việc triển khai cụ thể mà nhà máy trả về.


1

Trong ví dụ cụ thể này, bạn có cái được gọi là "Giá trị ma thuật". Về cơ bản, một giá trị được mã hóa cứng có thể thay đổi theo thời gian. Tôi sẽ cố gắng giải quyết câu hỏi hóc búa mà bạn thể hiện một cách khái quát, nhưng đây là một ví dụ về loại điều trong đó việc tạo một lớp con là công việc nhiều hơn là thay đổi một giá trị trong một lớp.

Nhiều khả năng, bạn đã chỉ định hành vi quá sớm trong hệ thống phân cấp lớp của bạn.

Hãy nói rằng chúng ta có PaycheckCalculator. Nhiều OvertimeFactorkhả năng sẽ bị khóa thông tin về nhân viên. Một nhân viên hàng giờ có thể được hưởng một phần thưởng ngoài giờ, trong khi một nhân viên được trả lương sẽ không được trả bất cứ điều gì. Tuy nhiên, một số nhân viên được trả lương sẽ có được thời gian thẳng vì hợp đồng mà họ đang làm việc. Bạn có thể quyết định rằng có một số loại kịch bản trả tiền đã biết và đó là cách bạn sẽ xây dựng logic của mình.

Trong PaycheckCalculatorlớp cơ sở, bạn làm cho nó trừu tượng và chỉ định các phương thức bạn mong đợi. Các tính toán cốt lõi là như nhau, chỉ là các yếu tố nhất định được tính khác nhau. HourlyPaycheckCalculatorSau đó, bạn sẽ thực hiện getOvertimeFactorphương thức và trả về 1.5 hoặc 2.0 như trường hợp của bạn. Bạn StraightTimePaycheckCalculatorsẽ thực hiện getOvertimeFactorđể trả về 1.0. Cuối cùng, việc thực hiện thứ ba sẽ là NoOvertimePaycheckCalculatorviệc thực hiện getOvertimeFactortrả về 0.

Điều quan trọng là chỉ mô tả hành vi trong lớp cơ sở dự định được mở rộng. Các chi tiết của các phần của thuật toán tổng thể hoặc các giá trị cụ thể sẽ được điền bởi các lớp con. Thực tế là bạn đã bao gồm một giá trị mặc định cho các getOvertimeFactorkhách hàng tiềm năng để "sửa chữa" nhanh chóng và dễ dàng cho một dòng thay vì mở rộng lớp như bạn dự định. Nó cũng nhấn mạnh thực tế là có nỗ lực liên quan đến việc mở rộng các lớp học. Ngoài ra còn có nỗ lực liên quan đến việc hiểu thứ bậc của các lớp trong ứng dụng của bạn. Bạn muốn thiết kế các lớp của mình theo cách để giảm thiểu nhu cầu tạo các lớp con nhưng vẫn cung cấp sự linh hoạt mà bạn cần.

Thực phẩm cho suy nghĩ: Khi các lớp học của chúng tôi gói gọn các yếu tố dữ liệu nhất định như OvertimeFactortrong ví dụ của bạn, bạn có thể cần một cách để lấy thông tin đó từ một số nguồn khác. Ví dụ: một tệp thuộc tính (vì nó trông giống như Java) hoặc cơ sở dữ liệu sẽ giữ giá trị và bạn PaycheckCalculatorsẽ sử dụng một đối tượng truy cập dữ liệu để lấy các giá trị của bạn. Điều này cho phép đúng người thay đổi hành vi của hệ thống mà không yêu cầu viết lại mã.

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.