2013-05-19 15 views
13

Tôi mới đến nguyên tắc SOLID nhưng tôi hiểu nó. Vấn đề chính của tôi là có một thời gian khó khăn thiết kế các lớp học của tôi để làm theo các SOLID đặc biệt là sự phụ thuộc Inversion. Đôi khi thật dễ dàng để viết toàn bộ logic vào khuôn mẫu thủ tục thay vì sử dụng SOLID.Làm thế nào để thực hành nguyên tắc SOLID của thiết kế OOP?

Ví dụ:

Hãy nói rằng chúng tôi đang tạo ra một hệ thống tham dự giám sát, và chúng tôi có logic (hoặc thủ tục) mà quét dấu vân tay người lao động, làm cho nó là ID, xác định có hay không đó là hợp lệ hay không, xác định anh ấy đang ở đâu, viết thông tin đăng nhập vào cơ sở dữ liệu và hiển thị nếu nó thành công hay không.

Thật dễ dàng để viết điều này một cách thủ tục với một loạt các 'nếu khác', vòng lặp và chuyển đổi. Nhưng trong tương lai tôi sẽ bị 'nợ mã'.

Nếu chúng tôi áp dụng nguyên tắc SOLID tại đây. Tôi biết rằng chúng ta cần phải có một loại đối tượng như 'AttendanceServiceClass' có một phương thức như 'scanEmployeeID()', 'processthislogin()' hoặc 'isItsucessful()'. Và tôi biết rằng lớp này có một sự phụ thuộc vào một kho lưu trữ, userinfo và các đối tượng khác.

Về cơ bản vấn đề của tôi là phân tích về thiết kế của lớp và phụ thuộc của nó

từng bước cách phân tích thiết kế của lớp học của bạn là gì?

xin lỗi vì tiếng Anh của tôi.

+0

RẮN là từ viết tắt của nhiều nguyên tắc. Bạn phải hiểu chúng trước rồi thử áp dụng chúng khi bạn viết mã. Công thức nấu ăn không thực sự tồn tại, bạn sẽ không nhận được từng bước làm thế nào để làm công cụ hoặc người nào khác sẽ không có nhu cầu cho các nhà phát triển con người. Mỗi ứng dụng và vấn đề đều có những thách thức và bối cảnh độc đáo, những gì làm việc trong một trường hợp có thể không hoạt động trong các trường hợp khác. – MikeSW

+0

Câu hỏi này có vẻ quá rộng để được trả lời hiệu quả. – anotherdave

+0

Hàng loạt các vids tốt trên SOLID có thể được tìm thấy ở đây: [DimeCasts.net] (http://dimecasts.net/Casts/ByTag/SOLID%20Principle) – PositiveGuy

Trả lời

7

Đôi khi thật dễ dàng để viết toàn bộ logic vào mẫu thủ tục chứ không phải để sử dụng RẮN

tôi không thể đồng ý hơn, nó dễ dàng hơn cho chúng ta lập trình để xử lý mã trong mô hình tố tụng. Điều này làm cho OOP khó cho lập trình viên quen với lập trình thủ tục.

Tuy nhiên tôi thấy dễ dàng hơn khi viết giao diện chung và người tiêu dùng đầu tiên thay vì phá vỡ giao diện được thiết kế cho các mô-đun nhỏ hơn. Đây là một loại thực hành Test First Development -> Red, green, refactor. (xin lưu ý rằng, nếu bạn muốn đạt được neat design, hãy xem xét sau TDD thay vì hướng dẫn này. Hướng dẫn này chỉ là một phần nhỏ làm TDD)

Giả sử rằng chúng ta muốn tạo ServiceAttendance làm scanEmployeeID. Chúng tôi sẽ có giao diện như (xin vui lòng lưu ý ví dụ là trong C# đặt tên):

public interface IServiceAttendance{ 
    bool ScanEmployeeId(); 
} 

Xin lưu ý rằng tôi quyết định phương thức trả lại bool thay vì để xác định thành công/thất bại. Xin lưu ý ví dụ tiêu dùng dưới đây không thực hiện bất kỳ DI vì tôi chỉ muốn cho thấy làm thế nào để tiêu thụ nó. Sau đó, trong người tiêu dùng, chúng tôi có thể có:

public void ConsumeServiceAttendance(){ 
    IServiceAttendance attendance = Resolve<IServiceAttendance>(); 
    if(attendance.ScanEmployeeId()){ 
     // do something 
    } 
} 

Điều này kết thúc người tiêu dùng. Bây giờ chúng tôi chuyển sang triển khai. Giả sử bạn có thể phát triển nó bằng cách sử dụng lập trình thủ tục và nhận khối mã nguyên khối. Bạn có thể tuyên bố thực hiện với câu lệnh giống như pseu.

public class ServiceAttendance : IServiceAttendance{ 
    public bool ScanEmployeeId(){ 
     bool isEmpValid = false; 
     // 1 scan the employee id 
     // 2 validate the login 
     // 3 if valid, create the login session 
     // 4 notify the user 
     return isEmpValid; 
    } 
} 

Bây giờ chúng tôi có 4 bước cần thực hiện trong thao tác này. Hiệu trưởng của tôi là, không được thực hiện trên 3 tiến trình mặt tiền trong một phương thức để tôi có thể đơn giản tái cấu trúc quy trình 3 và 4 thành một. Bây giờ chúng tôi có

public class ServiceAttendance : IServiceAttendance{ 
    public bool ScanEmployeeId(){ 
     bool isEmpValid = false; 
     // 1 scan the employee id 
     // 2 validate the login 
     // 3 if valid, create the login session and notify the user 
     return isEmpValid; 
    } 
} 

Điều này, chúng tôi có 3 hoạt động chính. Chúng ta có thể phân tích liệu chúng ta có cần tạo ra một mô-đun nhỏ hơn hay không bằng cách phá vỡ hoạt động. Nói rằng chúng ta muốn phá vỡ hoạt động thứ hai. Chúng tôi có thể nhận được:

// 2 validate the login 
// 2.1 check if employee id matches the format policy 
// 2.2 check if employee id exists in repository 
// 2.3 check if employee id valid to access the module 

Chính hoạt động phân tích đủ rõ ràng để phá mô-đun thứ hai thành mô-đun nhỏ hơn khác. Đối với 2.22.3, chúng tôi cần một mô-đun nhỏ hơn để được tiêm. Đơn giản vì nó sẽ cần sự phụ thuộc vào kho lưu trữ, do đó cần phải được tiêm. Trường hợp tương tự áp dụng cho bước hoạt động 1 scan the employee id, vì nó sẽ cần sự phụ thuộc vào máy quét vân tay, do đó trình xử lý máy quét phải được triển khai trong mô-đun riêng biệt.

Chúng ta có thể luôn luôn cố hoạt động, như chúng ta có thể làm điều đó trong 2.1:

// 2.1 check if employee id matches the format policy 
// 2.1.1 employee id must match the length 
// 2.1.2 employee id must has format emp##### 

Bây giờ tôi không chắc chắn nếu 2.1.12.1.2 cần phải được chia nhỏ thành 2 module tách ra, đó là tùy thuộc vào bạn để quyết định. Và bây giờ chúng ta có các giao diện, sau đó chúng ta có thể bắt đầu thực hiện. Mong đợi để ném exceptions trong quá trình xác nhận hoặc bạn sẽ cần phải vượt qua lớp tùy chỉnh để xử lý các thông báo lỗi.

+0

Ví dụ DI của bạn là sử dụng sai. Phụ thuộc phải được INJECTED làm đối số phương thức hoặc thông qua hàm tạo đối tượng. Chỉ trong một số trường hợp khi bạn xây dựng một khung công tác hoặc một thứ gì đó tương tự, nó phù hợp để sử dụng trực tiếp trình giải quyết. – MikeSW

+2

Tuyệt đối. Đó là lý do tại sao tôi đã đề cập trong ví dụ rằng 'Hãy chú ý ví dụ người tiêu dùng dưới đây không thực hiện bất kỳ DI vì tôi chỉ muốn cho thấy làm thế nào để tiêu thụ nó' – Fendy

28

Trước hết, rắn không phải là ONE nguyên tắc, nó tượng trưng cho 5 nguyên tắc khác nhau:

  • SRP (Single Trách nhiệm Nguyên tắc): lớp học của bạn nên chỉ có một trách nhiệm rõ ràng;
  • OCP (Nguyên tắc đóng mở): lớp học của bạn phải mở để gia hạn nhưng đóng để sửa đổi;
  • LSP (Nguyên tắc thay thế của Liskov): hướng dẫn này hướng dẫn bạn quyết định có sử dụng mối quan hệ thừa kế giữa lớp AB hay không. Thừa kế là phù hợp bất cứ khi nào tất cả các đối tượng của một lớp dẫn xuất B có thể được thay thế bằng các đối tượng của lớp cha mẹ của chúng A mà không mất bất kỳ tính năng nào;
  • ISP (Nguyên tắc phân đoạn giao diện): tuyên bố rằng không khách hàng nào bị buộc phải phụ thuộc vào các phương pháp không sử dụng;
  • DIP (Dependency Injection/Inversion): tuyên bố rằng các mô-đun cấp cao không nên phụ thuộc vào các mô-đun cấp thấp.

Các nguyên tắc này là hướng dẫn, nhưng điều đó không có nghĩa là bạn phải sử dụng chúng một cách nghiêm ngặt mọi lúc.

Từ mô tả của bạn, tôi có thể thấy khó khăn chính của bạn là suy nghĩ OO. Bạn vẫn đang suy nghĩ về cách làm thế nào để thực hiện mọi việc và đây là một suy nghĩ thủ tục . Nhưng trong OOP, điều quan trọng hơn là quyết định người sẽ làm những việc này.

Suy nghĩ về DI, sử dụng ví dụ của bạn, chúng ta hãy xem kịch bản của bạn:

public class AttendanceService { 
    // other stuff... 

    public boolean scanEmployeeId() { 
     // The scanning is made by an barcode reader on employee's name tag 
    } 
} 

vấn đề là gì đây?

Trước tiên, mã này vi phạm SRP: Điều gì xảy ra nếu quá trình xác thực thay đổi? Nếu công ty quyết định rằng thẻ tên là không an toàn và cài đặt một hệ thống nhận dạng sinh trắc học? Vâng, ở đây có một lý do để lớp của bạn thay đổi, nhưng lớp này không chỉ làm xác thực, nó còn làm những thứ khác, vì vậy, sẽ có một lý do khác để nó thay đổi. SRP nói rằng các lớp của bạn chỉ nên có MỘT lý do để thay đổi.

Nó cũng vi phạm OCP: Điều gì sẽ xảy ra nếu có phương thức xác thực khác có sẵn và tôi muốn có thể sử dụng như tôi muốn? Tôi không thể. Để thay đổi phương thức xác thực, tôi phải sửa đổi lớp.

Nó vi phạm ISP: Tại sao một đối tượng ServiceAttendance có một phương pháp để xác thực người lao động nếu nó chỉ cần cung cấp tham gia dịch vụ?


Hãy cải thiện nó một chút:

public class BarCodeAuth { 
    public boolean authenticate() { 
     // Authenticates... 
    } 
} 

public class AttendanceService { 
    private BarCodeAuth auth; 
    public AttendanceClass() { 
     this.auth = new BarCodeAuth(); 
    } 

    public void doOperation() { 
     if(this.auth.authenticate()) { 
      // do stuff.. 
     } 
    } 
} 

Bây giờ đó là một chút tốt hơn. Chúng tôi đã giải quyết được vấn đề với SRPISP, nhưng nếu bạn nghĩ tốt hơn, nó vẫn vi phạm OCP và bây giờ vi phạm DIP. Vấn đề là AttendanceService được kết hợp chặt chẽ với BarCodeAuth. Tôi vẫn không thể thay đổi phương thức xác thực mà không cần chạm vào AttendanceService.

Bây giờ chúng ta hãy áp dụng OCPDIP với nhau:

public interface AuthMethod { 
    public boolean authenticate(); 
} 

public class BarCodeAuth implements AuthMethod { 
    public boolean authenticate() { 
     // Authenticates... 
    } 
} 

public class BiometricAuth implements AuthMethod { 
    public boolean authenticate() { 
     // Authenticates... 
    } 
} 

public class FooBarAuth implements AuthMethod { 
    public boolean authenticate() { 
     // Authenticates... 
    } 
} 

public class AttendanceClass { 
    private AuthMethod auth; 
    public AttendanceClass(AuthMethod auth) { 
     this.auth = auth; 
    } 

    public void doOperation() { 
     if(this.auth.authenticate()) { 
      // do stuff.. 
     } 
    } 
} 

Bây giờ tôi có thể làm:

new AttendanceClass(new BarCordeAuth()); 
new AttendanceClass(new BiometricAuth()); 

Để thay đổi hành vi, tôi không cần phải chạm vào lớp. Nếu một số phương thức xác thực khác xuất hiện, tôi chỉ cần triển khai nó, tôn trọng giao diện và sẵn sàng sử dụng (hãy nhớ OCP?). Điều này là do tôi đang sử dụng DIP trên ServiceAttendance. Mặc dù nó cần một phương thức xác thực, nó không phải là khả năng của nó để tạo ra một phương thức xác thực. Trong hành động, đối với đối tượng này, nó không quan trọng phương pháp xác thực, nó chỉ cần biết nếu người gọi (người dùng) là hoặc không được phép làm những gì anh ta đang cố gắng làm.

Đây là tất cả về DIP là: các thành phần của bạn phải phụ thuộc vào trừu tượng, không triển khai.

+1

Tôi thích cách bạn bắt đầu với 'Những nguyên tắc này là hướng dẫn, nhưng nó không có nghĩa là bạn phải sử dụng chúng STRICTLY mỗi lần.'. Tôi hoàn toàn đồng ý, chúng tôi không thể luôn luôn thực hiện nó mọi lúc. Đôi khi chúng ta phải thỏa hiệp và làm những gì cần phải được thực hiện để đạt được một yêu cầu. Tôi cũng muốn giữ cho nó đơn giản (KIS) và không muốn quá hoàn hảo và kết thúc với một lớp in 'Hello' và một lớp khác in' World', đó chỉ là một cường điệu nhưng nó xảy ra. – JohnnyQ

3

Trước hết, hãy nghĩ đến các phần khác nhau của hệ thống tham dự. Giao diện người dùng, máy quét in ngón tay, kho lưu trữ cơ sở dữ liệu, quy trình đăng nhập và quy trình làm việc. Để thiết kế hệ thống này, chúng ta có thể bắt đầu thiết kế các bộ phận riêng biệt và kết nối chúng như một hệ thống.

Một thiết kế thô có thể là xung quanh các bộ phận của hệ thống như sau:

  • quét vân tay và Listener
  • Dịch vụ Attendance
  • Nhân viên Kho
  • Đăng nhập Repository
  • Giao diện người dùng
  • tham dự Bộ điều khiển quy trình làm việc
  • vân tay Chữ ký

Trong đoạn mã sau liệt kê một số khía cạnh của nguyên tắc thiết kế sẽ hiển thị đã:

  • SRP - Một tổ chức có trách nhiệm một công việc
  • LoD - Luật của Demeter - Chỉ nói chuyện cho bạn bè trực tiếp của bạn. Bạn sẽ thấy Controller không biết gì về kho.
  • DBC (Design by Contract) - Làm việc với giao diện
  • Sử dụng dependency injection và IoC - constructor injection và phương pháp tiêm
  • ISP (Interface Tách riêng Nguyên tắc) - Giao diện là nạc
  • OCP - Ghi đè phương thức giao diện trong nguồn gốc các lớp hoặc thực thi khác nhau như các giao diện được tiêm có thể mở rộng hành vi mà không cần sửa đổi lớp.

Dựa trên suy nghĩ nhiều này, hệ thống có thể làm việc như thế này:

[Bạn có thể tiếp tục cải thiện nó và thêm thiếu logic, tôi đang cung cấp một phác thảo thiết kế rất nhanh chóng với thực hiện ngắn gọn.]

interface IAttedanceController 
{ 
    run(); 
} 

interface IFingerprintHandler 
{ 
    void processFingerprint(IFingerprintSignature fingerprintSignature); 
} 

interface IFingerprintScanner 
{ 
    void run(IFingerprintHandler fingerprintHandler); 
} 

interface IAttendanceService 
{ 
    void startService(); 
    void stopService(); 
    bool attempEmployeeLogin(IFingerprintSignature fingerprintSignature); 
    string getFailureMessage(); 
} 

interface ILoginRepository 
{ 
    bool loginEmployee(IEmployee employee, DateTime timestamp); 
    void open(); 
    void close(); 
} 

interface IEmployeeRepository 
{ 
    IEmployee findEmployee(IFingerprintSignature fingerprintSignature); 
    void open(); 
    void close(); 
} 

//----------------------------------------- 

class AttendanceService : IAttendanceService 
{ 
    private IEmployeeRepository _employeeRepository; 
    private ILoginRepository _loginRepository; 
    private string _failureMessage; 

    public class AttendanceService(
     IEmployeeRepository employeeRepository, 
     ILoginRepository loginRepository) 
    { 
     this._employeeRepository = employeeRepository; 
     this._loginRepository = loginRepository; 
    } 

    public bool attempEmployeeLogin(IFingerprintSignature fingerprintSignature) 
    { 
     IEmployee employee = this._employeeRepository.findEmployee(fingerprintSignature); 

     if(employee != null) 
     { 
      //check for already logged in to avoid duplicate logins.. 

      this._loginRepository.loginEmployee(employee, DateTime.Now); 
      //or create a login record with timestamp and insert into login repository 

      return true; 
     } 
     else 
     { 
      this._failureMessage = "employee not found"; 
      return false; 
     } 
    } 

    public string getFailureMessage() 
    { 
     return "reason for failure"; 
    } 

    public void startService() 
    { 
     this._employeeRepository.open(); 
     this._loginRepository.open(); 
    } 

    public void stopService() 
    { 
     this._employeeRepository.close(); 
     this._loginRepository.close(); 
    } 
} 

//----------------------------------------- 

class AttendanceController : IAttedanceController, IFingerprintHandler 
{ 
    private ILoginView _loginView; 
    private IAttendanceService _attedanceService; 
    private IFingerprintScanner _fingerprintScanner; 

    public AttendanceController(
     ILoginView loginView, 
     IAttendanceService attendanceService, 
     IFingerprintScanner fingerprintScanner) 
    { 
     this._loginView = loginView; 
     this._attedanceService = attedanceService; 
     this._fingerprintScanner = fingerprintScanner; 
    } 

    public void run() 
    { 
     this._attedanceService.startService(); 
     this._fingerprintScanner.run(this); 
     this._loginView.show(); 
    } 

    public void IFingerprintHandler.processFingerprint(IFingerprintSignature fingerprintSignature) 
    { 
     if(this._attedanceService.login(fingerprintSignature)) 
     { 
     this._loginView.showMessage("Login successful"); 
     } 
     else 
     { 
     string errorMessage = string getFailureMessage(); 
     this._loginView.showMessage("errorMessage"); 
     } 

     // on return the fingerprint monitor is ready to take another finter print 
    } 
} 

//----------------------------------------- 

App.init() 
{ 
    // Run app bootstrap 
    // Initialize abstract factories or DI containers 

    IAttedanceController attedanceController = DIContainer.resolve("AttedanceController"); 
    attedanceController.run(); 
} 

//----------------------------------------- 
2

Chắc chắn, lập trình thủ tục dễ dàng hơn nhiều cho những người đang sử dụng để viết mã procedurally. Đối với những người được sử dụng để viết mã định hướng đối tượng tốt, mã thủ tục thực sự khó hơn.

Có, mã định hướng đối tượng được thừa nhận tốt thường dẫn đến nhiều công việc hơn và mã thực tế hơn. Nhưng nếu được thực hiện một cách chính xác, nó làm cho mã dễ bảo trì hơn, dễ mở rộng hơn, dễ dàng hơn để gỡ lỗi (và quan trọng hơn là dễ dàng hơn để kiểm tra).

7

Không cụ thể về SOLID, nhưng đáng nói đến như là một phương pháp đào tạo rất thú vị OOP- đào tạo bởi Jeff Bay: Object Oriented Calisthenics. Ý tưởng là bạn có thể cố gắng tuân theo các quy tắc rất nghiêm ngặt về một dự án nhỏ phi thực tế.

The Rules 

1. One level of indentation per method 
2. Don’t use the ELSE keyword 
3. Wrap all primitives and Strings 
4. First class collections 
5. One dot per line 
6. Don’t abbreviate 
7. Keep all entities small 
8. No classes with more than two instance variables 
9. No getters/setters/properties 

By ngừng hoài nghi, và cứng nhắc áp dụng các quy tắc trên, dự án đường nhỏ 1000, bạn sẽ bắt đầu thấy một cách tiếp cận khác nhau đáng kể để thiết kế phần mềm. Khi bạn đã viết 1000 dòng mã , bài tập được thực hiện và bạn có thể thư giãn và quay lại sử dụng 9 quy tắc này làm nguyên tắc.

Đây là bài tập khó, đặc biệt là vì nhiều quy tắc trong số này không thể áp dụng rộng rãi. Thực tế là, đôi khi các lớp học có ít hơn 50 dòng. Nhưng có giá trị lớn trong việc suy nghĩ về những gì sẽ xảy ra để di chuyển những trách nhiệm đó thành các đối tượng thực sự, hạng nhất của riêng chúng. Đó là phát triển loại suy nghĩ này là giá trị thực của bài tập . Vì vậy, kéo dài các giới hạn của những gì bạn tưởng tượng là có thể, và xem liệu bạn có bắt đầu nghĩ về mã của bạn theo một cách mới hay không.

+1

+1 cho việc giới thiệu về hướng đối tượng Calisthenics. Cảm ơn! –