如何练习OOP设计的SOLID原理?

时间:2013-05-19 19:58:25

标签: design-patterns dependency-injection solid-principles

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

例如:

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

很容易用一堆'if else',循环和开关以程序方式编写它。但是将来我会受到“代码债务”的影响。

如果我们在这里应用SOLID原则。我知道我们需要有一些像'AttendanceServiceClass'这样的对象,它有一个像'scanEmployeeID()','processthislogin()'或'isItsucessful()'这样的方法。我知道这个类依赖于存储库,userinfo和其他对象。

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

分析班级设计的一步一步是什么?

抱歉我的英语。

5 个答案:

答案 0 :(得分:36)

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

  • SRP(单一责任原则):您的班级应该只有一个明确的责任;
  • OCP(开放式原则):您的课程应该开放以进行扩展,但已关闭进行修改;
  • LSP(Liskov的替换原则):这个指导您决定是否使用类AB之间的继承关系。只要派生类B的所有对象都可以被其父类A的对象替换而没有任何功能损失,继承就是合适的;
  • ISP(接口隔离原则):声明不应强迫任何客户端依赖它不使用的方法;
  • 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 : 如果有另一种认证方法可以使用,我希望能够按照我的意愿使用该怎么办?我不能。要更改auth方法,我必须修改类。

违反 ISP : 为什么ServiceAttendance对象有一个员工身份验证方法,如果它只是提供服务出勤率?


让我们稍微改进一下:

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

现在好一点了。我们用 SRP ISP 解决了这些问题,但如果您认为更好,仍会违反 OCP ,现在违反 DIP 。问题是AttendanceServiceBarCodeAuth紧密耦合。我仍然无法在不触及AttendanceService的情况下更改身份验证方法。

现在让我们一起应用 OCP DIP

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

要改变行为,我不需要触摸课程。如果出现其他一些auth方法,我只需要实现它,尊重界面并准备使用(记住 OCP ?)。这是因为我在ServiceAttendance上使用 DIP 。虽然它需要一种身份验证方法,但创建一种方法并不是它的责任。实际上,对于这个对象,认证方法无关紧要,只需要知道调用者(用户)是否被授权做他正在尝试做的事情。

这完全是关于 DIP :你的组件应该依赖于抽象,而不是实现。

答案 1 :(得分:8)

不是专门针对SOLID,但值得一提的是Jeff Bay的一个非常有趣的OOP- 培训方法:Object Oriented Calisthenics。我们的想法是,您可以尝试在非现实的小项目中遵循一套非常严格的规则。

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行   代码,练习完成,你可以放松,然后回去使用   这九条规则作为指导方针。

     

这是一项艰苦的练习,尤其如此   因为其中许多规则并非普遍适用。事实   是的,有时班级略多于50行。但是有   思考移动那些必须发生的事情是非常有价值的   将责任转化为自己的真实,一流的对象。它的   发展这种类型的思想,这是真正的价值   行使。因此,伸展你想象的极限,并且   看看你是否开始以新的方式思考你的代码。

答案 2 :(得分:7)

  
    

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

  

我完全同意,程序员在程序模式中处理代码更容易。这使得习惯于程序编程的程序员很难使用OOP。

然而,我发现首先编写通用接口和消费者更容易,而不是破坏设计用于较小模块的接口。这是一种Test First Development -> Red, green, refactor练习。 (请注意,如果您想要达到neat design,请考虑使用TDD而不是本指南。本指南只是TDD的一小部分)

假设我们要创建ServiceAttendancescanEmployeeID。我们将有类似的界面(请注意示例是在C#命名中):

public interface IServiceAttendance{
    bool ScanEmployeeId();
}

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

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

消费者得出结论。现在我们转向实施。假设您可以使用过程编程开发它并获得单片代码块。您可以使用pseu-like语句声明实现。

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.12.1.2是否需要分解为2个独立的模块,由您自行决定。现在我们得到了接口,然后我们就可以开始实现了。期望在验证期间抛出exceptions,或者您需要传递自定义类来处理错误消息。

答案 3 :(得分:3)

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

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

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

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

  • SRP - 一个实体负责一项工作
  • LoD - 得墨忒耳定律 - 只与您的直接朋友交谈。您会注意到Controller对存储库一无所知。
  • DbC(按合同设计) - 针对接口工作
  • 使用依赖注入和IoC - 构造函数注入和方法注入
  • ISP(接口隔离原则) - 接口是精简的
  • 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();
}

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

答案 4 :(得分:2)

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

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