Tôi có nên chuyển một đối tượng vào một hàm tạo, hoặc khởi tạo trong lớp không?


10

Hãy xem xét hai ví dụ sau:

Truyền một đối tượng cho một nhà xây dựng

class ExampleA
{
  private $config;
  public function __construct($config)
  {
     $this->config = $config;
  }
}
$config = new Config;
$exampleA = new ExampleA($config);

Khởi tạo một lớp học

class ExampleB
{
  private $config;
  public function __construct()
  {
     $this->config = new Config;
  }
}
$exampleA = new ExampleA();

Đó là cách chính xác để xử lý việc thêm một đối tượng như một tài sản? Khi nào tôi nên sử dụng cái này hơn cái kia? Kiểm tra đơn vị có ảnh hưởng đến những gì tôi nên sử dụng?


Điều này có thể tốt hơn trên codereview.stackexchange.com/questions ?
Thất vọngWithFormsDesigner

9
@FrustratedWithFormsDesigner - điều này không phù hợp với Đánh giá mã.
ChrisF

Câu trả lời:


14

Tôi nghĩ rằng cái đầu tiên sẽ cung cấp cho bạn khả năng tạo một configđối tượng ở nơi khác và chuyển nó đến ExampleA. Nếu bạn cần Dependency Injection, đây có thể là một điều tốt vì bạn có thể đảm bảo rằng tất cả các mối quan hệ chia sẻ cùng một đối tượng.

Mặt khác, có thể bạn ExampleA yêu cầu một configđối tượng mới và sạch nên có thể có trường hợp ví dụ thứ hai phù hợp hơn, chẳng hạn như trường hợp mỗi trường hợp có thể có cấu hình khác nhau.


1
Vì vậy, A tốt cho thử nghiệm đơn vị, B tốt cho các trường hợp khác ... tại sao không có cả hai? Tôi thường có hai hàm tạo, một cho DI thủ công, một cho sử dụng bình thường (tôi sử dụng Unity cho DI, sẽ tạo các hàm tạo để đáp ứng các yêu cầu DI của nó).
Ed James

3
@EdWoodcock không thực sự là về thử nghiệm đơn vị so với các trường hợp khác. Đối tượng hoặc yêu cầu sự phụ thuộc từ bên ngoài hoặc nó quản lý nó ở bên trong. Không bao giờ cả hai hoặc một trong hai.
MattDavey

9

Đừng quên về khả năng kiểm tra!

Thông thường nếu hành vi của Examplelớp phụ thuộc vào cấu hình mà bạn muốn có thể kiểm tra nó mà không lưu / sửa đổi trạng thái bên trong của thể hiện của lớp (tức là bạn muốn có các kiểm tra đơn giản cho đường dẫn hạnh phúc và cho cấu hình sai mà không sửa đổi thuộc tính / người ghi nhớ của Examplelớp).

Vì vậy, tôi sẽ đi với tùy chọn đầu tiên của ExampleA


Đó là lý do tại sao tôi đề cập đến thử nghiệm - cảm ơn vì đã thêm nó :)
Tù nhân

5

Nếu đối tượng có trách nhiệm quản lý thời gian tồn tại của phụ thuộc, thì bạn có thể tạo đối tượng trong hàm tạo (và loại bỏ nó trong hàm hủy). *

Nếu đối tượng không chịu trách nhiệm quản lý vòng đời của sự phụ thuộc, thì nó sẽ được chuyển vào hàm tạo và được quản lý từ bên ngoài (ví dụ: bởi một bộ chứa IoC).

Trong trường hợp này, tôi không nghĩ ClassA của bạn phải chịu trách nhiệm tạo $ config, trừ khi nó cũng chịu trách nhiệm xử lý nó hoặc nếu cấu hình là duy nhất cho mỗi phiên bản ClassA.

* Để hỗ trợ khả năng kiểm tra, hàm tạo có thể tham chiếu đến lớp / phương thức của nhà máy để xây dựng sự phụ thuộc trong hàm tạo của nó, tăng sự gắn kết và khả năng kiểm tra.

//Object which manages the lifetime of its dependency (C#):
public class ClassA : IDisposable
{
    public Config Config { get; private set; }

    public ClassA()
    {
        this.Config = new Config(); // Tightly coupled to Config class...
    }

    public void Dispose()
    {
        this.Config.Dispose();
    }
}

// Object which does not manage its dependency:
public class ClassA
{
    public Config Config { get; set; }

    public ClassA(Config config) // dependency may be injected...
    {
        this.Config = config;
    }
}

// Object which manages its dependency in a testable way:
public class ClassA : IDisposable
{
    public Config Config { get; private set; }

    public ClassA(IConfigFactory configFactory) // dependency may be mocked...
    {
        this.Config = configFactory.BuildConfig();
    }

    public void Dispose()
    {
        this.Config.Dispose();
    }
}

2

Gần đây tôi đã có cuộc thảo luận tương tự với nhóm kiến ​​trúc của chúng tôi và có một số lý do tinh tế để thực hiện theo cách này hay cách khác. Nó chủ yếu được đưa vào Dependency Injection (như những người khác đã lưu ý) và liệu bạn có thực sự có quyền kiểm soát việc tạo đối tượng mà bạn mới trong hàm tạo hay không.

Trong ví dụ của bạn, điều gì sẽ xảy ra nếu lớp Cấu hình của bạn:

a) có phân bổ không tầm thường, chẳng hạn như đến từ một nhóm hoặc phương pháp nhà máy.

b) có thể không được phân bổ. Chuyển nó vào hàm tạo một cách gọn gàng để tránh vấn đề này.

c) thực sự là một lớp con của Cấu hình.

Truyền đối tượng vào hàm tạo cho sự linh hoạt nhất.


1

Câu trả lời dưới đây là sai, nhưng tôi sẽ giữ nó để người khác học hỏi từ nó (xem bên dưới)

Trong ExampleA, bạn có thể sử dụng cùng một Configthể hiện trên nhiều lớp. Tuy nhiên, nếu chỉ có một Configphiên bản trong toàn bộ ứng dụng, hãy xem xét áp dụng mẫu Singleton trên Configđể tránh có nhiều phiên bản Config. Và nếu Configlà Singleton, bạn có thể thực hiện các thao tác sau:

class ExampleA
{
  private $config;
  public function __construct()
  {
     $this->config = Config->getInstance();
  }
}
$exampleA = new ExampleA();

Trong ExampleB, mặt khác, bạn sẽ luôn có được một trường hợp riêng biệt của Configtừng thể hiện của ExampleB.

Phiên bản nào bạn nên áp dụng thực sự phụ thuộc vào cách ứng dụng sẽ xử lý các trường hợp Config:

  • nếu mỗi trường hợp ExampleXnên có một thể hiện riêng biệt Config, hãy đi với ExampleB;
  • nếu mỗi phiên bản ExampleXsẽ chia sẻ một (và chỉ một) phiên bản của Config, sử dụng ExampleA with Config Singleton;
  • nếu các trường hợp ExampleXcó thể sử dụng các trường hợp khác nhau Config, hãy gắn bó với ExampleA.

Tại sao chuyển đổi Configthành Singleton là sai:

Tôi phải thừa nhận rằng tôi chỉ học về mẫu Singleton ngày hôm qua (đọc cuốn sách Head First về các mẫu thiết kế). Chắc chắn tôi đã đi và áp dụng nó cho ví dụ này, nhưng như nhiều người đã chỉ ra, một cách là một cách khác (một số đã khó hiểu hơn và chỉ nói "Bạn đang làm sai!"), Đây không phải là một ý tưởng tốt. Vì vậy, để ngăn người khác mắc lỗi tương tự mà tôi vừa mắc phải, dưới đây là tóm tắt lý do tại sao mẫu Singleton có thể gây hại (dựa trên các nhận xét và những gì tôi đã phát hiện ra)

  1. Nếu ExampleAlấy tham chiếu riêng của nó đến Configthể hiện, các lớp sẽ được liên kết chặt chẽ. Sẽ không có cách nào để có một ExampleAphiên bản sử dụng một phiên bản khác của Config(giả sử một số lớp con). Điều này thật kinh khủng nếu bạn muốn kiểm tra ExampleAbằng cách sử dụng một ví dụ giả Configvì không có cách nào để cung cấp nó ExampleA.

  2. Tiền đề của việc sẽ có một, và chỉ một, ví dụ Configcó thể nắm giữ ngay bây giờ , nhưng bạn không thể luôn chắc chắn rằng điều tương tự sẽ giữ trong tương lai . Nếu tại một thời điểm nào đó, nó chỉ ra rằng nhiều trường hợp Configsẽ được mong muốn, không có cách nào để đạt được điều này mà không cần viết lại mã.

  3. Mặc dù trường hợp một và chỉ một Configcó thể đúng với mọi thời đại, nhưng có thể xảy ra rằng bạn muốn có thể sử dụng một số lớp con của Config(trong khi vẫn chỉ có một thể hiện). Nhưng, kể từ khi mã trực tiếp được thể hiện qua getInstance()các Config, mà là một staticphương pháp, không có cách nào để có được những lớp con. Một lần nữa, mã phải được viết lại.

  4. Thực tế là việc ExampleAsử dụng Configsẽ bị ẩn, ít nhất là khi chỉ xem API của ExampleA. Điều này có thể hoặc không phải là một điều xấu, nhưng cá nhân tôi cảm thấy rằng điều này cảm thấy như một bất lợi; chẳng hạn, khi duy trì, không có cách đơn giản nào để tìm ra lớp nào sẽ bị ảnh hưởng bởi các thay đổi Configmà không xem xét việc thực hiện của mọi lớp khác.

  5. Ngay cả khi việc ExampleAsử dụng Singleton Config không phải là vấn đề trong chính nó, nó vẫn có thể trở thành vấn đề từ quan điểm thử nghiệm. Các đối tượng Singleton sẽ mang trạng thái sẽ tồn tại cho đến khi chấm dứt ứng dụng. Đây có thể là một vấn đề khi chạy thử nghiệm đơn vị vì bạn muốn tách biệt một thử nghiệm với một thử nghiệm khác (nghĩa là đã thực hiện một thử nghiệm này sẽ không ảnh hưởng đến kết quả của thử nghiệm khác). Để khắc phục điều này, đối tượng Singleton phải bị hủy giữa mỗi lần chạy thử (có khả năng phải khởi động lại toàn bộ ứng dụng), điều này có thể tốn thời gian (không đề cập đến tẻ nhạt và gây phiền nhiễu).

Đã nói điều này, tôi rất vui vì tôi đã phạm sai lầm này ở đây và không phải trong việc thực hiện một ứng dụng thực sự. Trong thực tế, tôi đã thực sự xem xét việc viết lại mã mới nhất của mình để sử dụng mẫu Singleton cho một số lớp. Mặc dù tôi có thể dễ dàng hoàn nguyên các thay đổi (tất nhiên mọi thứ được lưu trữ trong SVN), tôi vẫn sẽ lãng phí thời gian để thực hiện.


4
Tôi không khuyên bạn nên làm như vậy .. theo cách này, bạn kết hợp chặt chẽ giữa lớp ExampleAConfig- đó không phải là một điều tốt.
Paul

@Paul: Đó là sự thật. Bắt tốt, đã không nghĩ về điều đó.
gablin

3
Tôi luôn khuyên bạn không nên sử dụng Singletons vì lý do kiểm tra. Chúng cơ bản là các biến toàn cầu và không thể chế giễu sự phụ thuộc.
MattDavey

4
Tôi luôn khuyên bạn nên sử dụng Singletons vì lý do bạn làm sai.
Raynos

1

Điều đơn giản nhất để làm là để vợ chồng ExampleAđến Config. Bạn nên làm điều đơn giản nhất, trừ khi có một lý do thuyết phục để làm điều gì đó phức tạp hơn.

Một lý do để tách rời ExampleAConfigsẽ là để cải thiện khả năng kiểm tra ExampleA. Khớp nối trực tiếp sẽ làm giảm khả năng kiểm tra ExampleAnếu Configcó các phương pháp chậm, phức tạp hoặc phát triển nhanh. Để thử nghiệm, một phương thức sẽ chậm nếu nó chạy nhiều hơn một vài micro giây. Nếu tất cả các phương pháp Configđơn giản và nhanh chóng, thì tôi sẽ sử dụng cách tiếp cận đơn giản và trực tiếp kết ExampleAhợp Config.


1

Ví dụ đầu tiên của bạn là một ví dụ về mẫu Tiêm phụ thuộc. Một lớp có sự phụ thuộc bên ngoài được trao cho sự phụ thuộc của hàm tạo, setter, v.v.

Cách tiếp cận này dẫn đến mã ghép lỏng lẻo. Hầu hết mọi người nghĩ rằng khớp nối lỏng lẻo là một điều tốt, bởi vì bạn có thể dễ dàng thay thế cấu hình trong trường hợp một trường hợp cụ thể của một đối tượng cần được cấu hình khác với các đối tượng khác, bạn có thể chuyển vào một đối tượng cấu hình giả để kiểm tra, và vì vậy trên.

Cách tiếp cận thứ hai gần hơn với mẫu người tạo GRASP. Trong trường hợp này, đối tượng tạo ra các phụ thuộc riêng của nó. Điều này dẫn đến mã được liên kết chặt chẽ, điều này có thể hạn chế tính linh hoạt của lớp và làm cho nó khó kiểm tra hơn. Nếu bạn cần một thể hiện của một lớp để có sự phụ thuộc khác với các lớp khác, thì lựa chọn duy nhất của bạn là phân lớp nó.

Tuy nhiên, nó có thể là mẫu thích hợp trong trường hợp tuổi thọ của đối tượng phụ thuộc bị quy định bởi tuổi thọ của đối tượng phụ thuộc và khi đối tượng phụ thuộc không được sử dụng ở bất kỳ đâu bên ngoài đối tượng phụ thuộc vào đối tượng đó. Thông thường tôi khuyên DI là vị trí mặc định, nhưng bạn không phải loại trừ hoàn toàn cách tiếp cận khác miễn là bạn nhận thức được hậu quả của nó.


0

Nếu lớp của bạn không hiển thị các $configlớp bên ngoài, thì tôi sẽ tạo nó bên trong hàm tạo. Bằng cách này, bạn đang giữ trạng thái nội bộ của mình ở chế độ riêng tư.

Nếu $configyêu cầu yêu cầu trạng thái bên trong của chính nó phải được đặt đúng (ví dụ: cần kết nối cơ sở dữ liệu hoặc một số trường bên trong được khởi tạo) trước khi sử dụng, thì nên trì hoãn việc khởi tạo thành một số mã bên ngoài (có thể là lớp nhà máy) và đưa nó vào người xây dựng. Hoặc, như những người khác đã chỉ ra, nếu nó cần được chia sẻ giữa các đối tượng khác.


0

Ví dụA được tách rời khỏi lớp Cấu hình cụ thể là tốt , đối tượng được cung cấp nhận được nếu không phải là loại Cấu hình mà là loại siêu lớp trừu tượng của Cấu hình.

ExampleB được kết hợp chặt chẽ với Cấu hình lớp cụ thể , điều này rất tệ .

Khởi tạo một đối tượng tạo ra một khớp nối mạnh mẽ giữa các lớp. Nó nên được thực hiện trong một lớp Factory.

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.