20

我是 SOLID 原则的新手,但我理解它。我的主要问题是很难设计我的课程来遵循 SOLID,特别是依赖倒置。有时将整个逻辑写入程序模式而不是使用 SOLID 很容易。

例如:

假设我们正在创建一个考勤监控系统,我们有逻辑(或程序)来扫描员工指纹,获取它的 ID,确定它是否有效,确定他在什么时间,将登录信息写入数据库,并显示它是否成功。

用一堆“if else”、循环和切换以程序方式编写它很容易。但在未来,我将遭受“代码债务”。

如果我们在这里应用 SOLID 原则。我知道我们需要某种对象,例如“AttendanceServiceClass”,该对象具有“scanEmployeeID()”、“processthislogin()”或“isItsucessful()”等方法。而且我知道这个类依赖于存储库、用户信息和其他对象。

基本上我的问题是分析类的设计及其依赖关系

分析您的班级设计的逐步方法是什么?

对不起我的英语不好。

4

5 回答 5

47

首先,solid 不是一个原则,它代表 5 个不同的原则:

  • SRP(单一职责原则):你的组件应该有一个单一的改变理由;
  • OCP(开闭原则):你的组件应该对扩展开放,对修改关闭;
  • LSP(Liskov's Substitution Principle):这个可以帮助你决定是否应该在类AB通过继承之间建立层次关系。B只要派生类的所有对象都可以被其父类的对象替换A而不会丢失任何功能,则继承适用;
  • ISP(Interface Segregation Principle):声明不应强迫任何组件依赖它不使用的方法;
  • DIP(依赖注入/反转):声明高级组件不应该依赖于低级组件。

这些原则是指导性的,但这并不意味着您必须每次都严格使用它们。

从您的描述中,我可以看出您的主要困难是思考 OO。你还在想怎么做,这是一种程序性的思维方式。但在 OOP 中,更重要的是决定谁来做这些事情。

考虑 DI,使用您的示例,让我们看看您的场景:

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

这里有什么问题?

好吧,首先,这段代码违反了SRP:如果身份验证过程发生变化怎么办?如果公司认为名称标签不安全并安装生物识别系统?好吧,你的类改变是有原因的,但是这个类不只是做身份验证,它做其他事情,所以,它会有其他的改变。SRP声明你的课程应该只有一个改变的理由。

它也违反了OCP:如果有另一种身份验证方法可用并且我希望能够按我的意愿使用怎么办?我不能。要更改身份验证方法,我必须修改类。

它违反了ISPServiceAttendance如果一个对象应该只提供服务出勤,为什么它有一种员工身份验证方法?


让我们稍微改进一下:

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..
        }
    }
}

现在好一点了。我们解决了SRPISP的问题,但如果你想得更好,它仍然违反了OCP,现在违反了DIP。问题在于它AttendanceServiceBarCodeAuth. 我仍然无法在不触摸的情况下更改 auth 方法AttendanceService

现在让我们一起应用OCPDIP

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..
        }
    }
}

现在我可以这样做:

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

要改变行为,我不需要接触班级。如果出现其他一些身份验证方法,我只需要实现它,尊重接口并且它可以使用(还记得OCP吗?)。这是因为ServiceAttendance. 尽管它需要一种身份验证方法,但它没有责任创建一个。实际上,对于这个对象,身份验证的方法并不重要,它只需要知道调用者(用户)是否被授权做他想做的事情。

这就是关于DIP的全部内容:您的组件应该依赖于抽象,而不是实现。

于 2013-05-20T01:07:37.610 回答
11

不是专门针对 SOLID,但值得一提的是 Jeff Bay 提出的一种非常有趣的 OOP训练方法:面向对象的健美操。这个想法是,您可以尝试在非现实生活中的小型项目上遵循一套非常严格的规则。

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

通过暂停怀疑,并将这些规则严格应用到一个 1000 行的小型项目中,您将开始看到一种截然不同的软件设计方法。一旦你编写了 1000 行代码,练习就完成了,你可以放松并重新使用这 9 条规则作为指导。

这是一项艰巨的练习,尤其是因为其中许多规则并不普遍适用。事实是,有时类会超过 50 行。但是考虑将这些职责转移到他们自己的真实的、一流的对象中会发生什么是很有价值的。培养这种思维方式才是练习的真正价值。所以扩展你想象的可能的极限,看看你是否开始以一种新的方式思考你的代码。

于 2013-05-30T23:33:41.427 回答
8

有时很容易将整个逻辑写入程序模式而不是使用 SOLID

我完全同意,对于我们程序员来说,以过程模式处理代码更容易。这使得 OOP 对于习惯于过程式编程的程序员来说很难。

然而,我发现首先编写通用接口和消费者比打破为较小模块设计的接口更容易。这是,一种Test First Development -> Red, green, refactor修行。(请注意,如果您想实现neat design,请考虑遵循 TDD 而不是本指南。本指南只是做 TDD 的一小部分)

假设我们要创建ServiceAttendanceto do scanEmployeeID。我们将有一个类似的接口(请注意该示例以 C# 命名):

public interface IServiceAttendance{
    bool ScanEmployeeId();
}

请注意,我决定返回 bool 而不是 void 来确定成功/失败操作的方法。请注意下面的消费者示例没有实现任何 DI,因为我只是想展示如何使用它。然后在消费者中,我们可以有:

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

消费者到此结束。现在我们开始执行。假设您可以使用过程编程开发它并获得整体代码块。您可以使用类似 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;
    }
}

现在我们在这一操作中有 4 个步骤要完成。我的原则是,不要在一种方法中完成超过 3 个外观过程,因此我可以简单地将 3 和 4 重构为一个过程。现在我们有

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;
    }
}

这个,我们有3个主要操作。我们可以通过分解操作来分析是否需要创建更小的模块。假设我们要中断第二个操作。我们可以得到:

// 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

分解操作本身很明显,足以将第二个模块分解成另一个更小的模块。对于2.22.3,我们需要注入一个更小的模块。仅仅因为它需要对存储库的依赖,因此需要注入。同样的情况也适用于操作步骤1 scan the employee id,因为它需要依赖指纹扫描仪,所以扫描仪处理程序必须在单独的模块中实现。

我们总是可以分解操作,因为我们可以这样做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#####

现在我不确定是否2.1.1并且2.1.2需要将其分解为 2 个独立的模块,由您决定。现在我们有了接口,然后我们就可以开始实现了。期望exceptions在验证期间抛出,否则您将需要传递自定义类来处理错误消息。

于 2013-05-20T02:39:13.593 回答
3

首先,考虑考勤系统的不同部分。用户界面、指纹扫描仪、数据库存储库、登录过程和工作流程。为了设计这个系统,我们可以开始孤立地设计零件并将它们连接为一个系统。

粗略的设计可能围绕系统的以下部分:

  • 指纹扫描仪和监听器
  • 考勤服务
  • 员工资料库
  • 登录存储库
  • 用户界面
  • 考勤工作流程控制器
  • 指纹签名

在以下代码清单中,设计原则的某些方面已经可见:

  • SRP - 一个实体负责一项工作
  • LoD - 得墨忒耳法则 - 只与你的直系朋友交谈。您会注意到 Controller 对存储库一无所知。
  • DbC(按合同设计) - 针对接口工作
  • 使用依赖注入和 IoC - 构造函数注入和方法注入
  • ISP (Interface Segregation Principle) - 接口是精简的
  • OCP - 覆盖派生类中的接口方法或传递不同的实现,因为注入的接口可以扩展行为而无需修改类。

基于这样的想法,系统可能会像这样工作:

[您可以进一步改进它并添加缺少的逻辑,我提供了一个非常快速的设计大纲和简短的实现。]

代码清单

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();
}

//-----------------------------------------
于 2013-05-20T03:53:02.130 回答
2

当然,对于习惯于按程序编写代码的人来说,过程式编程要容易得多。对于那些习惯于编写良好分解的面向对象代码的人来说,过程代码实际上更难。

是的,精心设计的面向对象代码通常会带来更多的工作和更实际的代码。但如果做得正确,它会使代码更容易维护、更容易扩展、更容易调试(更重要的是更容易测试)。

于 2013-05-20T04:15:52.333 回答