Nguyên tắc trách nhiệm duy nhất. Không đơn giản như nó có vẻ

Nguyên tắc trách nhiệm duy nhất. Không đơn giản như nó có vẻ Nguyên tắc trách nhiệm duy nhất hay còn gọi là nguyên tắc trách nhiệm duy nhất,
hay còn gọi là nguyên tắc biến đổi đồng đều - một anh chàng cực kỳ khó hiểu và là một câu hỏi đầy lo lắng trong một cuộc phỏng vấn lập trình viên.

Lần đầu tiên tôi làm quen nghiêm túc với nguyên tắc này diễn ra vào đầu năm thứ nhất, khi những đứa trẻ còn non nớt được đưa vào rừng để đào tạo những học sinh thành ấu trùng - những học sinh thực sự.

Trong rừng, chúng tôi được chia thành các nhóm 8-9 người mỗi nhóm và thi đấu - nhóm nào sẽ uống một chai vodka nhanh nhất, với điều kiện là người đầu tiên trong nhóm rót vodka vào ly, người thứ hai uống hết, và người thứ ba có một bữa ăn nhẹ. Đơn vị đã hoàn thành hoạt động của mình sẽ di chuyển đến cuối hàng đợi của nhóm.

Trường hợp kích thước hàng đợi là bội số của ba là cách triển khai SRP tốt.

Định nghĩa 1. Trách nhiệm duy nhất.

Định nghĩa chính thức của Nguyên tắc Trách nhiệm duy nhất (SRP) nêu rõ rằng mỗi thực thể có trách nhiệm và lý do tồn tại riêng và nó chỉ có một trách nhiệm.

Xét đối tượng “Người uống rượu” (người uống rượu).
Để thực hiện nguyên tắc SRP, chúng tôi sẽ chia trách nhiệm thành ba:

  • Một người đổ (ĐổHoạt động)
  • Một ly (UốngLênHoạt Động)
  • Một người có một bữa ăn nhẹ (CắnHoạt Động)

Mỗi người tham gia quy trình chịu trách nhiệm về một thành phần của quy trình, nghĩa là có một trách nhiệm nguyên tử - uống, rót hoặc ăn nhẹ.

Ngược lại, hố uống rượu là một mặt tiền cho các hoạt động này:

сlass Tippler {
    //...
    void Act(){
        _pourOperation.Do() // налить
        _drinkUpOperation.Do() // выпить
        _takeBiteOperation.Do() // закусить
    }
}

Nguyên tắc trách nhiệm duy nhất. Không đơn giản như nó có vẻ

Tại sao?

Lập trình viên con người viết mã cho người vượn, còn người vượn thì thiếu chú ý, ngu ngốc và luôn vội vàng. Bé có thể nhớ và hiểu khoảng 3 - 7 thuật ngữ cùng một lúc.
Trong trường hợp người say rượu, có ba thuật ngữ như vậy. Tuy nhiên, nếu chúng ta viết mã bằng một tờ giấy thì nó sẽ chứa bàn tay, chiếc kính, những trận đánh nhau và những cuộc tranh cãi bất tận về chính trị. Và tất cả điều này sẽ nằm trong nội dung của một phương thức. Tôi chắc rằng bạn đã từng thấy đoạn mã như vậy trong quá trình thực hành của mình. Không phải là bài kiểm tra nhân đạo nhất đối với tâm lý.

Mặt khác, người vượn được thiết kế để mô phỏng các vật thể trong thế giới thực trong đầu anh ta. Trong trí tưởng tượng của mình, anh ta có thể đẩy chúng lại với nhau, lắp ráp các đồ vật mới từ chúng và tháo rời chúng theo cách tương tự. Hãy tưởng tượng một chiếc xe mô hình cũ. Trong trí tưởng tượng của mình, bạn có thể mở cửa, tháo phần viền cửa và nhìn thấy ở đó các cơ cấu nâng cửa sổ, bên trong sẽ có các bánh răng. Nhưng bạn không thể xem tất cả các thành phần của máy cùng một lúc trong một “danh sách”. Ít nhất thì “người khỉ” không thể.

Do đó, các lập trình viên con người phân tách các cơ chế phức tạp thành một tập hợp các phần tử hoạt động và ít phức tạp hơn. Tuy nhiên, nó có thể bị phân hủy theo nhiều cách khác nhau: ở nhiều ô tô cũ, ống dẫn khí đi vào cửa và ở ô tô hiện đại, lỗi khóa điện tử khiến động cơ không thể khởi động, điều này có thể gây ra sự cố trong quá trình sửa chữa.

Bây giờ, SRP là một nguyên tắc giải thích CÁCH phân rã, tức là vẽ đường phân chia ở đâu.

Ông cho rằng cần phải phân hủy theo nguyên tắc phân chia “trách nhiệm”, tức là theo nhiệm vụ của một số đối tượng nhất định.

Nguyên tắc trách nhiệm duy nhất. Không đơn giản như nó có vẻ

Hãy quay trở lại việc uống rượu và những lợi ích mà người khỉ nhận được trong quá trình phân hủy:

  • Mã đã trở nên cực kỳ rõ ràng ở mọi cấp độ
  • Mã có thể được viết bởi nhiều lập trình viên cùng một lúc (mỗi người viết một phần tử riêng biệt)
  • Kiểm tra tự động được đơn giản hóa - phần tử càng đơn giản thì càng dễ kiểm tra
  • Thành phần của mã xuất hiện - bạn có thể thay thế UốngLênHoạt Động đến một hoạt động trong đó một người say rượu đổ chất lỏng dưới bàn. Hoặc thay thế thao tác rót bằng thao tác trong đó bạn trộn rượu và nước hoặc rượu vodka và bia. Tùy thuộc vào yêu cầu kinh doanh, bạn có thể làm mọi thứ mà không cần chạm vào mã phương thức Tippler.Act.
  • Từ những thao tác này, bạn có thể loại bỏ kẻ háu ăn (chỉ sử dụng TakeBitHoạt động), Có cồn (chỉ sử dụng UốngLênHoạt Động trực tiếp từ chai) và đáp ứng nhiều yêu cầu kinh doanh khác.

(Ồ, có vẻ như đây đã là nguyên tắc OCP rồi, và tôi đã vi phạm trách nhiệm của bài đăng này)

Và tất nhiên là có nhược điểm:

  • Chúng ta sẽ phải tạo ra nhiều loại hơn.
  • Một người say uống rượu lần đầu tiên muộn hơn vài giờ so với bình thường.

Định nghĩa 2. Tính biến thiên thống nhất.

Cho phép tôi, các quý ông! Lớp uống rượu cũng có một trách nhiệm duy nhất - đó là uống rượu! Và nhìn chung, từ “trách nhiệm” là một khái niệm vô cùng mơ hồ. Có người chịu trách nhiệm về số phận của nhân loại, có người chịu trách nhiệm nuôi dưỡng những chú chim cánh cụt bị lật úp ở cột điện.

Hãy xem xét hai cách thực hiện của người uống rượu. Loại đầu tiên, được đề cập ở trên, bao gồm ba lớp - rót, uống và ăn nhẹ.

Phần thứ hai được viết thông qua phương pháp “Chuyển tiếp và chỉ chuyển tiếp” và chứa tất cả logic trong phương thức Hành động:

//Не тратьте время  на изучение этого класса. Лучше съешьте печеньку
сlass BrutTippler {
   //...
   void Act(){
        // наливаем
    if(!_hand.TryDischarge(from:_bottle, to:_glass, size:_glass.Capacity))
        throw new OverdrunkException();

    // выпиваем
    if(!_hand.TryDrink(from: _glass,  size: _glass.Capacity))
        throw new OverdrunkException();

    //Закусываем
    for(int i = 0; i< 3; i++){
        var food = _foodStore.TakeOrDefault();
        if(food==null)
            throw new FoodIsOverException();

        _hand.TryEat(food);
    }
   }
}

Cả hai tầng lớp này, dưới góc nhìn của người quan sát bên ngoài, trông giống hệt nhau và có chung trách nhiệm “uống rượu”.

Lú lẫn!

Sau đó, chúng ta lên mạng và tìm hiểu một định nghĩa khác về SRP - Nguyên tắc có thể thay đổi duy nhất.

SCP tuyên bố rằng "Một mô-đun có một và chỉ một lý do để thay đổi". Đó là “Trách nhiệm là lý do để thay đổi”.

(Có vẻ như những người đưa ra định nghĩa ban đầu rất tin tưởng vào khả năng ngoại cảm của người vượn)

Bây giờ mọi thứ rơi vào vị trí. Riêng biệt, chúng ta có thể thay đổi quy trình rót, uống và ăn vặt, nhưng trong bản thân người uống rượu, chúng ta chỉ có thể thay đổi trình tự và thành phần của các hoạt động, chẳng hạn như bằng cách di chuyển đồ ăn nhẹ trước khi uống hoặc thêm đọc bánh mì nướng.

Trong phương pháp “Chuyển tiếp và Chỉ chuyển tiếp”, mọi thứ có thể thay đổi chỉ được thay đổi trong phương thức Hành động. Điều này có thể dễ đọc và hiệu quả khi có ít logic và hiếm khi thay đổi, nhưng thường nó kết thúc bằng những phương pháp khủng khiếp, mỗi dòng 500 dòng, với nhiều câu lệnh if hơn mức cần thiết để Nga gia nhập NATO.

Định nghĩa 3. Bản địa hóa các thay đổi.

Những người uống rượu thường không hiểu tại sao họ lại thức dậy trong căn hộ của người khác hoặc điện thoại di động của họ ở đâu. Đã đến lúc thêm nhật ký chi tiết.

Hãy bắt đầu đăng nhập với quá trình đổ:

class PourOperation: IOperation{
    PourOperation(ILogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.Log($"Before pour with {_hand} and {_bottle}");
        //Pour business logic ...
        _log.Log($"After pour with {_hand} and {_bottle}");
    }
}

Bằng cách gói gọn nó trong ĐổHoạt động, chúng tôi đã hành động khôn ngoan từ quan điểm trách nhiệm và sự đóng gói, nhưng bây giờ chúng tôi đang bối rối với nguyên tắc biến đổi. Ngoài bản thân hoạt động có thể thay đổi, bản thân việc ghi nhật ký cũng có thể thay đổi. Bạn sẽ phải tách và tạo một logger đặc biệt cho thao tác đổ:

interface IPourLogger{
    void LogBefore(IHand, IBottle){}
    void LogAfter(IHand, IBottle){}
    void OnError(IHand, IBottle, Exception){}
}

class PourOperation: IOperation{
    PourOperation(IPourLogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.LogBefore(_hand, _bottle);
        try{
             //... business logic
             _log.LogAfter(_hand, _bottle");
        }
        catch(exception e){
            _log.OnError(_hand, _bottle, e)
        }
    }
}

Người đọc kỹ càng sẽ nhận thấy rằng Đăng nhậpSau, Đăng nhậpTrước и BậtLỗi cũng có thể được thay đổi riêng lẻ và tương tự với các bước trước đó, sẽ tạo ra ba lớp: ĐổLoggerTrước, ĐổLoggerSau и ĐổErrorLogger.

Và nhớ rằng có ba thao tác đối với một người uống rượu, chúng ta có được chín lớp ghi nhật ký. Kết quả là toàn bộ vòng uống rượu bao gồm 14 lớp (!!!).

Hyperbol? Khắc nghiệt! Một người đàn ông khỉ với một quả lựu đạn phân hủy sẽ chia “người rót” thành một cái decanter, một chiếc ly, người vận hành rót, dịch vụ cấp nước, một mô hình vật lý về sự va chạm của các phân tử và trong quý tiếp theo, anh ta sẽ cố gắng gỡ rối các phụ thuộc mà không cần các biến toàn cục. Và tin tôi đi, anh ấy sẽ không dừng lại.

Đến đây, nhiều người đi đến kết luận rằng SRP là câu chuyện cổ tích từ vương quốc màu hồng, đi xa chơi mì…

... mà không hề biết đến sự tồn tại của định nghĩa thứ ba về Srp:

“Nguyên tắc trách nhiệm duy nhất nêu rõ rằng những thứ tương tự như thay đổi nên được lưu trữ ở một nơi". hoặc "Những thay đổi nào cùng nhau nên được giữ ở một nơi"

Nghĩa là, nếu chúng ta thay đổi nhật ký của một thao tác thì chúng ta phải thay đổi nó ở một nơi.

Đây là một điểm rất quan trọng - vì tất cả những lời giải thích về SRP ở trên đều nói rằng cần phải nghiền nát các loại trong khi chúng đang bị nghiền nát, tức là họ đã áp đặt “giới hạn trên” đối với kích thước của vật thể, và bây giờ chúng ta đang nói về “giới hạn thấp hơn”. Nói cách khác, SRP không chỉ yêu cầu “vừa nghiền vừa nghiền” mà còn không được lạm dụng quá mức – “không nghiền nát những thứ lồng vào nhau”. Đây là trận chiến vĩ đại giữa lưỡi dao cạo của Occam và người vượn!

Nguyên tắc trách nhiệm duy nhất. Không đơn giản như nó có vẻ

Bây giờ người uống sẽ cảm thấy tốt hơn. Ngoài thực tế là không cần chia logger IPourLogger thành ba lớp, chúng ta cũng có thể kết hợp tất cả các logger thành một loại:

class OperationLogger{
    public OperationLogger(string operationName){/*..*/}
    public void LogBefore(object[] args){/*...*/}       
    public void LogAfter(object[] args){/*..*/}
    public void LogError(object[] args, exception e){/*..*/}
}

Và nếu chúng ta thêm loại hoạt động thứ tư, thì việc ghi nhật ký cho nó đã sẵn sàng. Và bản thân mã hoạt động sạch sẽ và không có tiếng ồn từ cơ sở hạ tầng.

Kết quả là chúng ta có 5 lớp để giải quyết vấn đề uống rượu:

  • Hoạt động đổ
  • Hoạt động uống rượu
  • Hoạt động gây nhiễu
  • Tiều phu
  • mặt tiền uống rượu

Mỗi người trong số họ chịu trách nhiệm nghiêm ngặt về một chức năng và có một lý do để thay đổi. Tất cả các quy tắc tương tự như thay đổi đều nằm gần đó.

Ví dụ thực tế cuộc sống

Chúng tôi đã từng viết một dịch vụ để tự động đăng ký khách hàng b2b. Và một phương pháp GOD đã xuất hiện cho 200 dòng nội dung tương tự:

  • Vào 1C và tạo tài khoản
  • Với tài khoản này, hãy chuyển đến mô-đun thanh toán và tạo nó ở đó
  • Kiểm tra xem tài khoản có tài khoản như vậy chưa được tạo trên máy chủ chính
  • Tạo tài khoản mới
  • Thêm kết quả đăng ký vào module thanh toán và số 1c vào dịch vụ kết quả đăng ký
  • Thêm thông tin tài khoản vào bảng này
  • Tạo số điểm cho khách hàng này trong dịch vụ điểm. Chuyển số tài khoản 1c của bạn cho dịch vụ này.

Và còn có thêm khoảng 10 hoạt động kinh doanh nữa trong danh sách này với khả năng kết nối khủng khiếp. Hầu như tất cả mọi người đều cần đối tượng tài khoản. ID điểm và tên khách hàng là cần thiết trong một nửa số cuộc gọi.

Sau một giờ tái cấu trúc, chúng tôi có thể tách mã cơ sở hạ tầng và một số sắc thái khi làm việc với tài khoản thành các phương thức/lớp riêng biệt. Phương pháp của Chúa khiến việc này trở nên dễ dàng hơn, nhưng vẫn còn 100 dòng mã không muốn gỡ rối.

Chỉ sau một vài ngày, người ta mới thấy rõ rằng bản chất của phương pháp “nhẹ nhàng” này là một thuật toán kinh doanh. Và mô tả ban đầu về các thông số kỹ thuật khá phức tạp. Và việc cố gắng chia phương pháp này thành nhiều phần sẽ vi phạm SRP chứ không phải ngược lại.

Chủ nghĩa hình thức.

Đã đến lúc để cơn say của chúng ta được yên. Hãy lau khô nước mắt - chúng ta chắc chắn sẽ quay lại vào một ngày nào đó. Bây giờ hãy chính thức hóa kiến ​​thức từ bài viết này.

Chủ nghĩa hình thức 1. Định nghĩa SRP

  1. Tách các phần tử sao cho mỗi phần tử chịu trách nhiệm về một việc.
  2. Trách nhiệm tượng trưng cho “lý do để thay đổi”. Nghĩa là, mỗi phần tử chỉ có một lý do để thay đổi, xét về mặt logic nghiệp vụ.
  3. Những thay đổi tiềm năng đối với logic kinh doanh. phải được bản địa hóa. Các phần tử thay đổi đồng bộ phải ở gần nhau.

Chủ nghĩa hình thức 2. Tiêu chí tự kiểm tra cần thiết.

Tôi chưa thấy có đủ tiêu chí để hoàn thành SRP. Nhưng có những điều kiện cần thiết:

1) Hãy tự hỏi lớp/phương thức/mô-đun/dịch vụ này làm gì. bạn phải trả lời nó bằng một định nghĩa đơn giản. ( Cảm ơn Brightori )

lời giải thích

Tuy nhiên, đôi khi rất khó tìm được một định nghĩa đơn giản

2) Sửa lỗi hoặc thêm tính năng mới sẽ ảnh hưởng đến số lượng tệp/lớp tối thiểu. Lý tưởng nhất - một.

lời giải thích

Vì trách nhiệm (đối với một tính năng hoặc lỗi) được gói gọn trong một tệp/lớp, nên bạn biết chính xác nơi cần tìm và nội dung cần chỉnh sửa. Ví dụ: tính năng thay đổi đầu ra của hoạt động ghi nhật ký sẽ chỉ yêu cầu thay đổi trình ghi nhật ký. Không cần phải chạy qua phần còn lại của mã.

Một ví dụ khác là thêm điều khiển giao diện người dùng mới, tương tự như các điều khiển trước đó. Nếu điều này buộc bạn phải thêm 10 thực thể khác nhau và 15 bộ chuyển đổi khác nhau thì có vẻ như bạn đang làm quá sức.

3) Nếu một số nhà phát triển đang làm việc trên các tính năng khác nhau của dự án của bạn thì khả năng xảy ra xung đột hợp nhất, tức là khả năng cùng một tệp/lớp sẽ bị một số nhà phát triển thay đổi cùng một lúc là rất nhỏ.

lời giải thích

Nếu khi thêm thao tác mới “Đổ vodka dưới bàn”, bạn cần tác động đến người ghi nhật ký, thao tác uống và rót thì có vẻ như trách nhiệm được phân chia một cách quanh co. Tất nhiên, điều này không phải lúc nào cũng thực hiện được nhưng chúng ta nên cố gắng giảm con số này xuống.

4) Khi được hỏi một câu hỏi làm rõ về logic nghiệp vụ (từ nhà phát triển hoặc người quản lý), bạn sẽ đi sâu vào một lớp/tệp và chỉ nhận thông tin từ đó.

lời giải thích

Các tính năng, quy tắc hoặc thuật toán được viết ngắn gọn, mỗi tính năng ở một nơi và không có cờ nằm ​​rải rác trong không gian mã.

5) Việc đặt tên rõ ràng.

lời giải thích

Lớp hoặc phương thức của chúng ta chịu trách nhiệm về một việc và trách nhiệm đó được phản ánh qua tên của nó

AllManagersManagerService - rất có thể là lớp God
LocalPayment - có thể là không

Chủ nghĩa hình thức 3. Phương pháp phát triển Occam-đầu tiên.

Khi bắt đầu thiết kế, người khỉ không biết và không cảm nhận được hết sự tinh tế của vấn đề đang được giải quyết và có thể mắc sai lầm. Bạn có thể mắc lỗi theo nhiều cách khác nhau:

  • Làm cho các đối tượng trở nên quá lớn bằng cách hợp nhất các trách nhiệm khác nhau
  • Tái cấu trúc bằng cách chia một trách nhiệm thành nhiều loại khác nhau
  • Xác định sai ranh giới trách nhiệm

Điều quan trọng là phải nhớ quy tắc: “thà phạm sai lầm lớn” hoặc “nếu bạn không chắc chắn thì đừng chia tay”. Ví dụ: nếu lớp của bạn có hai trách nhiệm thì điều đó vẫn có thể hiểu được và có thể được chia thành hai với những thay đổi tối thiểu đối với mã máy khách. Việc lắp ráp một chiếc kính từ các mảnh kính thường khó khăn hơn do ngữ cảnh trải rộng trên nhiều tệp và thiếu các phụ thuộc cần thiết trong mã máy khách.

Đã đến lúc kết thúc một ngày

Phạm vi của SRP không giới hạn ở OOP và SOLID. Nó áp dụng cho các phương thức, hàm, lớp, mô-đun, microservice và dịch vụ. Nó áp dụng cho cả sự phát triển “figax-figax-and-prod” và “khoa học tên lửa”, giúp thế giới tốt đẹp hơn một chút ở mọi nơi. Nếu bạn nghĩ về nó, đây gần như là nguyên tắc cơ bản của mọi kỹ thuật. Kỹ thuật cơ khí, hệ thống điều khiển và thực tế là tất cả các hệ thống phức tạp đều được xây dựng từ các thành phần, và “phân mảnh quá mức” làm mất đi tính linh hoạt của các nhà thiết kế, “phân mảnh quá mức” làm mất đi hiệu quả của các nhà thiết kế và các ranh giới không chính xác làm mất đi lý trí và sự an tâm của họ.

Nguyên tắc trách nhiệm duy nhất. Không đơn giản như nó có vẻ

SRP không phải do tự nhiên phát minh ra và không phải là một phần của khoa học chính xác. Nó phá vỡ những giới hạn sinh học và tâm lý của chúng ta, nó chỉ là một cách để kiểm soát và phát triển các hệ thống phức tạp bằng cách sử dụng bộ não của người vượn. Anh ấy cho chúng tôi biết cách phân hủy một hệ thống. Công thức ban đầu yêu cầu một lượng lớn thần giao cách cảm, nhưng tôi hy vọng bài viết này sẽ giải quyết được một số vấn đề.

Nguồn: www.habr.com

Thêm một lời nhận xét