Tôi có làm cho lớp học của tôi quá chi tiết không? Nguyên tắc trách nhiệm duy nhất nên được áp dụng như thế nào?


9

Tôi viết rất nhiều mã bao gồm ba bước cơ bản.

  1. Lấy dữ liệu từ một nơi nào đó.
  2. Chuyển đổi dữ liệu đó.
  3. Đặt dữ liệu đó ở đâu đó.

Tôi thường kết thúc bằng ba loại lớp học - lấy cảm hứng từ các mẫu thiết kế tương ứng của chúng.

  1. Các nhà máy - để xây dựng một đối tượng từ một số tài nguyên.
  2. Hòa giải - để sử dụng nhà máy, thực hiện chuyển đổi, sau đó sử dụng chỉ huy.
  3. Chỉ huy - để đặt dữ liệu đó ở một nơi khác.

Các lớp học của tôi có xu hướng khá nhỏ, thường là một phương thức (công khai), ví dụ: lấy dữ liệu, chuyển đổi dữ liệu, thực hiện công việc, lưu dữ liệu. Điều này dẫn đến sự gia tăng của các lớp học, nhưng nói chung hoạt động tốt.

Nơi tôi đấu tranh là khi tôi đến thử nghiệm, cuối cùng tôi sẽ kết hợp chặt chẽ các bài kiểm tra. Ví dụ;

  • Nhà máy - đọc các tập tin từ đĩa.
  • Chỉ huy - ghi tập tin vào đĩa.

Tôi không thể kiểm tra cái này mà không có cái kia. Tôi có thể viết mã 'kiểm tra' bổ sung để đọc / ghi đĩa, nhưng sau đó tôi đang lặp lại.

Nhìn vào .Net, lớp File có một cách tiếp cận khác, nó kết hợp các trách nhiệm (của tôi) nhà máy và chỉ huy với nhau. Nó có các chức năng Tạo, Xóa, Tồn tại và Đọc tất cả ở một nơi.

Tôi có nên tìm cách làm theo ví dụ về .Net và kết hợp - đặc biệt là khi xử lý các tài nguyên bên ngoài - các lớp của tôi với nhau không? Mã nó vẫn được ghép nối, nhưng nó có chủ ý hơn - nó xảy ra ở lần thực hiện ban đầu, thay vì trong các thử nghiệm.

Có phải vấn đề của tôi ở đây là tôi đã áp dụng Nguyên tắc Trách nhiệm Đơn lẻ một cách quá nhiệt tình? Tôi có các lớp riêng chịu trách nhiệm đọc và viết. Khi tôi có thể có một lớp kết hợp chịu trách nhiệm xử lý một tài nguyên cụ thể, ví dụ như đĩa hệ thống.



6
Looking at .Net, the File class takes a different approach, it combines the responsibilities (of my) factory and commander together. It has functions for Create, Delete, Exists, and Read all in one place.- Lưu ý rằng bạn đang kết hợp "trách nhiệm" với "việc cần làm". Một trách nhiệm giống như một "lĩnh vực quan tâm." Trách nhiệm của lớp Tệp là thực hiện các thao tác tệp.
Robert Harvey

1
Có vẻ như tôi đang ở trong tình trạng tốt. Tất cả những gì bạn cần là một trung gian kiểm tra (hoặc một cho mọi loại chuyển đổi nếu bạn thích điều đó tốt hơn). Hòa giải thử nghiệm có thể đọc các tệp để xác minh tính chính xác của chúng, sử dụng lớp Tệp của .net. Không có vấn đề với điều đó từ quan điểm RẮN.
Martin Maat

1
Như được đề cập bởi @Robert Harvey, SRP có một cái tên nhảm nhí vì nó không thực sự liên quan đến Trách nhiệm. Đó là về "đóng gói và trừu tượng hóa một lĩnh vực quan tâm khó khăn / khó khăn duy nhất có thể thay đổi". Tôi đoán STDACMC quá dài. :-) Điều đó nói rằng, tôi nghĩ rằng sự phân chia của bạn thành ba phần có vẻ hợp lý.
dùng949300

1
Một điểm quan trọng trong Filethư viện của bạn từ C # là, đối với tất cả những gì chúng ta biết, Filelớp chỉ có thể là một mặt tiền, đặt tất cả các hoạt động tệp vào một nơi duy nhất - vào lớp, nhưng có thể sử dụng một lớp đọc / ghi tương tự cho lớp của bạn. thực sự chứa logic phức tạp hơn để xử lý tập tin. Lớp như vậy Filevẫn sẽ tuân thủ SRP, bởi vì quá trình thực sự làm việc với hệ thống tập tin sẽ được trừu tượng hóa sau một lớp khác - rất có thể với giao diện hợp nhất. Không nói đó là trường hợp, nhưng nó có thể. :)
Andy

Câu trả lời:


5

Theo nguyên tắc Trách nhiệm duy nhất có thể là những gì hướng dẫn bạn ở đây nhưng nơi bạn có một tên khác.

Phân đoạn trách nhiệm truy vấn lệnh

Hãy nghiên cứu điều đó và tôi nghĩ rằng bạn sẽ thấy nó theo một mô hình quen thuộc và bạn không đơn độc trong việc tự hỏi bao lâu để thực hiện điều này. Thử nghiệm axit là nếu làm theo điều này sẽ mang lại cho bạn lợi ích thực sự hoặc nếu đó chỉ là một câu thần chú mù quáng mà bạn tuân theo để bạn không phải suy nghĩ.

Bạn đã bày tỏ mối quan tâm về thử nghiệm. Tôi không nghĩ rằng việc tuân theo CQRS sẽ ngăn chặn việc viết mã có thể kiểm tra được. Bạn có thể chỉ cần theo dõi CQRS theo cách làm cho mã của bạn không thể kiểm tra được.

Nó giúp biết cách sử dụng đa hình để đảo ngược các phụ thuộc mã nguồn mà không cần thay đổi luồng điều khiển. Tôi không thực sự chắc chắn nơi bộ kỹ năng của bạn đang trong bài kiểm tra viết.

Một lời cảnh báo, theo các thói quen bạn tìm thấy trong các thư viện là không tối ưu. Thư viện có nhu cầu riêng của họ và thẳng thắn cũ. Vì vậy, ngay cả ví dụ tốt nhất cũng chỉ là ví dụ tốt nhất từ ​​hồi đó.

Điều này không có nghĩa là không có ví dụ hoàn toàn hợp lệ không tuân theo CQRS. Theo sau nó sẽ luôn là một chút đau đớn. Nó không phải luôn luôn là một giá trị phải trả. Nhưng nếu bạn cần nó, bạn sẽ rất vui vì bạn đã sử dụng nó.

Nếu bạn sử dụng nó, hãy chú ý đến lời cảnh báo này:

Cụ thể, CQRS chỉ nên được sử dụng trên các phần cụ thể của hệ thống (BoundedContext trong biệt ngữ DDD) chứ không phải toàn bộ hệ thống. Theo cách nghĩ này, mỗi Bối cảnh bị ràng buộc cần có quyết định riêng về cách nó nên được mô hình hóa.

Martin Flowler: CQRS


Thú vị không thấy CQRS trước. Mã này có thể kiểm tra được, đây là về việc cố gắng tìm một cách tốt hơn. Tôi sử dụng giả, và tiêm phụ thuộc khi tôi có thể (mà tôi nghĩ là những gì bạn đang đề cập đến).
James Wood

Lần đầu tiên đọc về điều này, tôi đã xác định được một thứ tương tự thông qua ứng dụng của mình: xử lý các tìm kiếm linh hoạt, các trường đa dạng có thể lọc / sắp xếp, (Java / JPA) là một vấn đề đau đầu và dẫn đến hàng tấn mã soạn sẵn, trừ khi bạn tạo một công cụ tìm kiếm cơ bản sẽ xử lý công cụ này cho bạn (tôi sử dụng rsql-jpa). Mặc dù tôi có cùng một mô hình (nói cùng một Thực thể JPA cho cả hai), các tìm kiếm được trích xuất trên một dịch vụ chung chuyên dụng và lớp mô hình không phải xử lý nữa.
Walfrat

3

Bạn cần một viễn cảnh rộng hơn để xác định xem mã có tuân thủ Nguyên tắc Trách nhiệm Đơn lẻ hay không. Không thể trả lời chỉ bằng cách phân tích chính mã, bạn phải xem xét lực lượng hoặc tác nhân nào có thể khiến các yêu cầu thay đổi trong tương lai.

Hãy nói rằng bạn lưu trữ dữ liệu ứng dụng trong một tệp XML. Những yếu tố nào có thể khiến bạn thay đổi mã liên quan đến đọc hoặc viết? Một số khả năng:

  • Mô hình dữ liệu ứng dụng có thể thay đổi khi các tính năng mới được thêm vào ứng dụng.
  • Các loại dữ liệu mới - ví dụ như hình ảnh - có thể được thêm vào mô hình
  • Định dạng lưu trữ có thể thay đổi độc lập với logic ứng dụng: Nói từ XML sang JSON hoặc sang định dạng nhị phân, do mối quan tâm về khả năng tương tác hoặc hiệu suất.

Trong tất cả các trường hợp này, bạn sẽ phải thay đổi cả cách đọc và logic viết. Nói cách khác, họ không phải là trách nhiệm riêng biệt.

Nhưng hãy tưởng tượng một kịch bản khác: Ứng dụng của bạn là một phần của đường ống xử lý dữ liệu. Nó đọc một số tệp CSV được tạo bởi một hệ thống riêng biệt, thực hiện một số phân tích và xử lý và sau đó xuất ra một tệp khác được xử lý bởi hệ thống thứ ba. Trong trường hợp này, đọc và viết là trách nhiệm độc lập và nên được tách rời.

Điểm mấu chốt: Nói chung, bạn không thể nói nếu việc đọc và ghi tệp là trách nhiệm riêng biệt, nó phụ thuộc vào vai trò trong ứng dụng. Nhưng dựa trên gợi ý của bạn về thử nghiệm, tôi đoán đó là trách nhiệm duy nhất trong trường hợp của bạn.


2

Nói chung bạn có ý kiến ​​đúng.

Lấy dữ liệu từ một nơi nào đó. Chuyển đổi dữ liệu đó. Đặt dữ liệu đó ở đâu đó.

Âm thanh như bạn có ba trách nhiệm. IMO "Người hòa giải" có thể làm rất nhiều. Tôi nghĩ bạn nên bắt đầu bằng cách mô hình hóa ba trách nhiệm của mình:

interface Reader[T] {
    def read(): T
}

interface Transformer[T, U] {
    def transform(t: T): U
}

interface Writer[T] {
    def write(t: T): void
}

Sau đó, một chương trình có thể được thể hiện như sau:

def program[T, U](reader: Reader[T], 
                  transformer: Transformer[T, U], 
                  writer: Writer[U]): void =
    writer.write(transformer.transform(reader.read()))

Điều này dẫn đến sự gia tăng của các lớp

Tôi không nghĩ đây là một vấn đề. IMO rất nhiều lớp nhỏ gắn kết, có thể kiểm tra tốt hơn các lớp lớn, ít gắn kết hơn.

Nơi tôi đấu tranh là khi tôi đến thử nghiệm, cuối cùng tôi sẽ kết hợp chặt chẽ các bài kiểm tra. Tôi không thể kiểm tra cái này mà không có cái kia.

Mỗi mảnh nên được kiểm tra độc lập. Được mô hình ở trên, bạn có thể biểu thị việc đọc / ghi vào một tệp như:

class FileReader(fileName: String) implements Reader[String] {
    override read(): String = // read file into string
}

class FileWriter(fileName: String) implements Writer[String] {
    override write(str: String) = // write str to file
}

Bạn có thể viết các bài kiểm tra tích hợp để kiểm tra các lớp này để xác minh chúng đọc và ghi vào hệ thống tập tin. Phần còn lại của logic có thể được viết dưới dạng biến đổi. Ví dụ: nếu các tệp có định dạng JSON, bạn có thể chuyển đổi Strings.

class JsonParser implements Transformer[String, Json] {
    override transform(str: String): Json = // parse as json
}

Sau đó, bạn có thể chuyển đổi thành các đối tượng thích hợp:

class FooParser implements Transformer[Json, Foo] {
    override transform(json: Json): Foo = // ...
}

Mỗi trong số này là độc lập có thể kiểm tra. Bạn cũng có thể kiểm tra đơn vị programtrên bằng cách chế giễu reader, transformerwriter.


Đó là khá nhiều nơi tôi đang ở ngay bây giờ. Tôi có thể kiểm tra từng chức năng riêng lẻ, tuy nhiên bằng cách kiểm tra chúng, chúng trở nên được ghép nối. Ví dụ, để FileWriter được kiểm tra, sau đó một cái gì đó phải đọc những gì đã được viết, giải pháp rõ ràng là sử dụng FileReader. Fwiw, hòa giải viên thường làm một cái gì đó khác như áp dụng logic kinh doanh hoặc có lẽ được đại diện bởi ứng dụng cơ bản Chức năng chính.
James Wood

1
@JamesWood đó thường là trường hợp với các bài kiểm tra tích hợp. Tuy nhiên, bạn không cần phải kết hợp các lớp trong bài kiểm tra. Bạn có thể kiểm tra FileWriterbằng cách đọc trực tiếp từ hệ thống tập tin thay vì sử dụng FileReader. Nó thực sự phụ thuộc vào bạn những mục tiêu của bạn đang được thử nghiệm. Nếu bạn sử dụng FileReader, kiểm tra sẽ bị hỏng nếu một trong hai FileReaderhoặc FileWriterbị hỏng - có thể mất nhiều thời gian hơn để gỡ lỗi.
Samuel

Ngoài ra, hãy xem stackoverflow.com/questions/1087351/ từ nó có thể giúp các bài kiểm tra của bạn đẹp hơn
Samuel

Đó là khá nhiều nơi tôi đang ở hiện tại - điều đó không đúng 100%. Bạn nói rằng bạn đang sử dụng mẫu Người hòa giải. Tôi nghĩ rằng điều này không hữu ích ở đây; mẫu này được sử dụng khi bạn có nhiều đối tượng khác nhau tương tác với nhau trong một luồng rất khó hiểu; bạn đặt một hòa giải viên ở đó để tạo điều kiện cho tất cả các mối quan hệ và thực hiện chúng ở một nơi. Đây dường như không phải là trường hợp của bạn; bạn có đơn vị nhỏ xác định rất rõ. Ngoài ra, giống như nhận xét ở trên của @Samuel, bạn nên kiểm tra một đơn vị và thực hiện các xác nhận của mình mà không gọi cho các đơn vị khác
Emerson Cardoso

@EmersonCardoso; Tôi đã phần nào đơn giản hóa kịch bản trong câu hỏi của tôi. Trong khi một số hòa giải viên của tôi khá đơn giản, những người khác thì phức tạp hơn và thường sử dụng nhiều nhà máy / chỉ huy. Tôi đang cố gắng tránh chi tiết của một kịch bản duy nhất, tôi quan tâm nhiều hơn đến kiến ​​trúc thiết kế cấp cao hơn có thể được áp dụng cho nhiều kịch bản.
James Wood

2

Tôi kết thúc sẽ kiểm tra kết hợp chặt chẽ. Ví dụ;

  • Nhà máy - đọc các tập tin từ đĩa.
  • Chỉ huy - ghi tập tin vào đĩa.

Vì vậy, trọng tâm ở đây là những gì được ghép chúng lại với nhau . Bạn có vượt qua một đối tượng giữa hai (chẳng hạn như a Filekhông?) Sau đó, đó là Tệp mà chúng được ghép nối chứ không phải nhau.

Từ những gì bạn đã nói bạn đã tách các lớp học của bạn. Cái bẫy là bạn đang thử nghiệm chúng cùng nhau bởi vì nó dễ dàng hơn hoặc 'có ý nghĩa' .

Tại sao bạn cần đầu vào Commanderđến từ một đĩa? Tất cả những gì nó quan tâm là viết bằng một đầu vào nhất định, sau đó bạn có thể xác minh nó đã ghi tệp chính xác bằng cách sử dụng những gì trong bài kiểm tra .

Phần thực tế mà bạn đang kiểm tra Factorylà 'nó có đọc đúng tệp này không và xuất ra đúng thứ'? Vì vậy, giả lập các tập tin trước khi đọc nó trong bài kiểm tra .

Ngoài ra, kiểm tra rằng Factory và Commander hoạt động khi kết hợp với nhau là tốt - nó phù hợp với Kiểm thử tích hợp khá vui vẻ. Câu hỏi ở đây là vấn đề đơn vị của bạn có thể kiểm tra chúng một cách riêng biệt hay không.


Trong ví dụ cụ thể đó, thứ gắn kết chúng lại với nhau là tài nguyên - ví dụ như đĩa hệ thống. Nếu không, không có tương tác giữa hai lớp.
James Wood

1

Lấy dữ liệu từ một nơi nào đó. Chuyển đổi dữ liệu đó. Đặt dữ liệu đó ở đâu đó.

Đó là một cách tiếp cận thủ tục điển hình, một cách mà David Parnas đã viết vào năm 1972. Bạn tập trung vào cách mọi thứ đang diễn ra. Bạn lấy giải pháp cụ thể cho vấn đề của mình như một mô hình cấp cao hơn, điều này luôn luôn sai.

Nếu bạn theo đuổi cách tiếp cận hướng đối tượng, tôi muốn tập trung vào miền của mình . Cái này chủ yếu là gì? Trách nhiệm chính của hệ thống của bạn là gì? Các khái niệm chính hiện diện trong ngôn ngữ của các chuyên gia tên miền của bạn là gì? Vì vậy, hãy hiểu tên miền của bạn, phân tách nó, coi các khu vực trách nhiệm cấp cao hơn như các mô-đun của bạn , coi các khái niệm cấp thấp hơn được biểu thị dưới dạng danh từ làm đối tượng của bạn. Đây là một ví dụ tôi cung cấp cho một câu hỏi gần đây, nó rất phù hợp.

Và có một vấn đề rõ ràng với sự gắn kết, bạn đã tự đề cập đến nó. Nếu bạn thực hiện một số sửa đổi là logic đầu vào và viết thử nghiệm trên đó, thì không có cách nào chứng minh rằng chức năng của bạn hoạt động, vì bạn có thể quên chuyển dữ liệu đó sang lớp tiếp theo. Hãy xem, những lớp này về bản chất được ghép nối. Và một sự tách rời nhân tạo làm cho mọi thứ thậm chí còn tồi tệ hơn. Tôi biết rằng bản thân mình: dự án 7 yo với 100 năm sau vai tôi được viết hoàn toàn theo phong cách này. Chạy khỏi nó nếu bạn có thể.

Và trên toàn bộ điều SRP. Đó là tất cả về sự gắn kết được áp dụng cho không gian vấn đề của bạn, tức là miền. Đó là nguyên tắc cơ bản đằng sau SRP. Điều này dẫn đến các đối tượng là thông minh và thực hiện trách nhiệm của họ đối với bản thân họ. Không ai kiểm soát họ, không ai cung cấp cho họ dữ liệu. Họ kết hợp dữ liệu và hành vi, chỉ phơi bày cái sau. Vì vậy, các đối tượng của bạn kết hợp cả xác thực dữ liệu thô, chuyển đổi dữ liệu (nghĩa là hành vi) và sự kiên trì. Nó có thể trông giống như sau:

class FinanceTransaction
{
    private $id;
    private $storage;

    public function __construct(UUID $id, DataStorage $storage)
    {
        $this->id = $id;
        $this->storage = $storage;
    }

    public function perform(
        Order $order,
        Customer $customer,
        Merchant $merchant
    )
    {
        if ($order->isExpired()) {
            throw new Exception('Order expired');
        }

        if ($customer->canNotPurchase($order)) {
            throw new Exception('It is not legal to purchase this kind of stuff by this customer');
        }

        $this->storage->save($this->id, $order, $customer, $merchant);
    }
}

(new FinanceTransaction())
    ->perform(
        new Order(
            new Product(
                $_POST['product_id']
            ),
            new Card(
                new CardNumber(
                    $_POST['card_number'],
                    $_POST['cvv'],
                    $_POST['expires_at']
                )
            )
        ),
        new Customer(
            new Name(
                $_POST['customer_name']
            ),
            new Age(
                $_POST['age']
            )
        ),
        new Merchant(
            new MerchantId($_POST['merchant_id'])
        )
    )
;

Kết quả là có khá nhiều lớp gắn kết đại diện cho một số chức năng. Lưu ý rằng xác nhận thường đi đến các đối tượng giá trị - ít nhất là trong phương pháp DDD .


1

Nơi tôi đấu tranh là khi tôi đến thử nghiệm, cuối cùng tôi sẽ kết hợp chặt chẽ các bài kiểm tra. Ví dụ;

  • Nhà máy - đọc các tập tin từ đĩa.
  • Chỉ huy - ghi tập tin vào đĩa.

Cảnh giác với sự trừu tượng bị rò rỉ khi làm việc với hệ thống tệp - Tôi thấy nó bị bỏ quên quá thường xuyên và nó có các triệu chứng bạn đã mô tả.

Nếu lớp hoạt động trên dữ liệu đến từ / đi vào các tệp này thì hệ thống tệp sẽ trở thành chi tiết triển khai (I / O) và nên được tách ra khỏi nó. Các lớp này (nhà máy / chỉ huy / hòa giải viên) không nên biết về hệ thống tệp trừ khi công việc duy nhất của họ là lưu trữ / đọc dữ liệu được cung cấp. Các lớp liên quan đến hệ thống tệp sẽ đóng gói các tham số cụ thể theo ngữ cảnh như các đường dẫn (có thể được truyền qua hàm tạo), vì vậy giao diện không tiết lộ bản chất của nó (từ "Tệp" trong tên giao diện hầu hết là một mùi).


"Các lớp này (nhà máy / chỉ huy / hòa giải viên) không nên biết về hệ thống tệp trừ khi công việc duy nhất của họ là lưu trữ / đọc dữ liệu được cung cấp." Trong ví dụ cụ thể này, đó là tất cả những gì họ đang làm.
James Wood

0

Theo ý kiến ​​của tôi, có vẻ như bạn đã bắt đầu đi xuống con đường đúng nhưng bạn đã không đi đủ xa. Tôi nghĩ rằng việc chia nhỏ chức năng thành các lớp khác nhau để làm một việc và làm tốt điều đó là chính xác.

Để tiến thêm một bước, bạn nên tạo giao diện cho các lớp Factory, Mediator và Commander. Sau đó, bạn có thể sử dụng các phiên bản giả định của các lớp đó khi viết bài kiểm tra đơn vị của mình cho việc triển khai cụ thể của các lớp khác. Với các giả định, bạn có thể xác thực rằng các phương thức được gọi theo đúng thứ tự và với các tham số chính xác và mã được kiểm tra hoạt động đúng với các giá trị trả về khác nhau.

Bạn cũng có thể nhìn vào việc trừu tượng hóa việc đọc / ghi dữ liệu. Bây giờ bạn sẽ đến một hệ thống tệp nhưng có thể muốn truy cập cơ sở dữ liệu hoặc thậm chí là một ổ cắm trong tương lai. Lớp hòa giải của bạn không cần phải thay đổi nếu nguồn / đích của dữ liệu thay đổi.


1
YAGNI là điều bạn nên suy nghĩ.
whatsisname
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.