Подтвердить что ты не робот

Как практиковать SOLID-принцип дизайна ООП?

Я новичок в принципе SOLID, но я это понимаю. Моя основная проблема заключается в том, что мне сложно разрабатывать мои классы, чтобы следовать за SOLID, особенно с инверсией зависимостей. Иногда легко написать всю логику в процедурный шаблон, а не использовать SOLID.

Например:

Скажем, что мы создаем систему мониторинга посещаемости, и у нас есть логика (или процедура), которая сканирует отпечаток сотрудника, получает его идентификатор, определяет, действительно ли он или нет, определить, в какое время он был, напишите войдите в базу данных и покажите, успешно ли это или нет.

Легко написать это процедурно с кучей "if else", loop и switch. Но в будущем я буду страдать от "кодового долга".

Если мы применим здесь принцип SOLID. Я знаю, что нам нужен какой-то объект типа "AttendanceServiceClass", который имеет такой метод, как "scanEmployeeID()", "processthislogin()" или "isItsucessful()". И я знаю, что этот класс имеет зависимость от репозитория, userinfo и других объектов.

В основном моя проблема заключается в анализе дизайна класса и его зависимостей

Что такое пошаговый способ анализа дизайна вашего класса?

Извините за мой английский.

4b9b3361

Ответ 1

Иногда легко записать всю логику в процедурный шаблон, а не использовать SOLID

Я не могу согласиться больше, нам легче программировать код в процедурной структуре. Это делает ООП трудным для программиста, который привык к процедурному программированию.

Однако мне было проще сначала написать общий интерфейс и потребитель, а не сломать интерфейс, предназначенный для небольших модулей. Это своего рода практика Test First Development -> Red, green, refactor. (обратите внимание, что если вы хотите достичь neat design, рассмотрите следующий TDD вместо этого руководства. Это руководство представляет собой лишь небольшую часть TDD)

Скажем, что мы хотим создать ServiceAttendance для выполнения scanEmployeeID. У нас будет интерфейс вроде (пожалуйста, обратите внимание, что пример относится к именованию С#):

public interface IServiceAttendance{
    bool ScanEmployeeId();
}

Пожалуйста, отметьте, что я решил, что метод возвращает bool вместо void для определения успеха/отказа. Обратите внимание, что пример потребителя ниже не реализует никакого DI, потому что я просто хочу показать, как его использовать. Тогда у потребителя мы можем:

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

Это завершает потребитель. Теперь мы переходим к реализации. Скажите, что вы можете разработать его с помощью процедурного программирования и получить монолитный кодовый блок. Вы можете указать реализацию с помощью псевдоподобного оператора.

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 и 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.2 и 2.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 во время проверки или вам потребуется передать пользовательский класс для обработки сообщений об ошибках.

Ответ 2

Прежде всего, твердая - это не ОДИН принцип, а 5 различных принципов:

  • SRP (принцип единой ответственности): в вашем классе должна быть только одна четко определенная ответственность;
  • OCP (принцип Open-Closed): ваш класс должен быть открыт для расширения, но закрыт для модификации;
  • LSP (принцип подстановки Лискова): этот совет поможет вам решить, использовать ли наследственные отношения между классами A и B. Наследование подходит всякий раз, когда все объекты производного класса B могут быть заменены объектами их родительского класса A без потери функциональности;
  • ISP (принцип разделения интерфейса): утверждает, что ни один клиент не должен зависеть от методов, которые он не использует;
  • DIP (Dependency Injection/Inversion): указывает, что модули высокого уровня не должны зависеть от модулей низкого уровня.

Эти принципы являются руководством, но это не значит, что вы должны использовать их строго каждый раз.

Из твоего описания я вижу твою главную трудность в том, чтобы думать ОО. Вы все еще думаете о , как делать что-то, и это процедурное мышление. Но в ООП более важно решить, кто будет делать эти вещи.

Думая о DI, используя ваш пример, давайте рассмотрим ваш сценарий:

public class AttendanceService {
    // other stuff...

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

В чем здесь проблема?

Ну, во-первых, этот код нарушает SRP: Что если процесс аутентификации изменится? Если компания решила, что бирки имен небезопасны, и установила систему биометрического распознавания? Что ж, здесь есть причина для изменения вашего класса, но этот класс не выполняет только аутентификацию, он выполняет другие действия, поэтому будут другие причины для его изменения. SRP утверждает, что у ваших классов должна быть только ОДНА причина для изменения.

Это также нарушает OCP: Что если есть другой способ аутентификации, и я хочу использовать его по своему усмотрению? Я не могу Чтобы изменить метод auth, мне нужно изменить класс.

Это нарушает интернет-провайдера: Почему у объекта 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 и интернет-провайдером, но если вы подумаете лучше, это все равно нарушает OCP и теперь нарушает DIP. Проблема в том, что AttendanceService тесно связан с BarCodeAuth. Я все еще не могу изменить метод аутентификации, не касаясь 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());

Чтобы изменить поведение, мне не нужно трогать класс. Если появляется какой-то другой метод аутентификации, мне просто нужно реализовать его, соблюдая интерфейс, и он готов к использованию (помните OCP?). Это из-за того, что я использую DIP на ServiceAttendance. Хотя для этого требуется метод аутентификации, он не обязан создавать его. На самом деле, для этого объекта не имеет значения метод аутентификации, ему просто нужно знать, авторизован ли вызывающий (пользователь) делать то, что он пытается сделать.

Это все, что связано с DIP: ваши компоненты должны зависеть от абстракций, а не от реализаций.

Ответ 3

Не определенно о SOLID, но стоит упомянуть очень интересный подход OOP обучения Джефф Бэй: Object Oriented Calisthenics a > . Идея состоит в том, что вы можете попытаться следовать множеству очень строгих правил в малом проекте, не относящемся к реальной жизни.

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 строк. Но theres большое значение в размышлении о том, что должно произойти, чтобы переместить эти ответственности в реальные, первоклассные объекты. это развивая этот тип мышления, а реальную ценность упражнение. Так что растягивайте границы того, что вы себе представляете, и посмотрите, начинаете ли вы думать о своем коде по-новому.

Ответ 4

Прежде всего, подумайте о разных частях системы посещаемости. Пользовательский интерфейс, сканер отпечатков пальцев, репозиторий базы данных, процесс входа в систему и рабочий процесс. Для разработки этой системы мы можем начать проектирование деталей по отдельности и подключить их как систему.

Грубая конструкция может быть вокруг следующих частей системы:

  • Сканер и прослушиватель отпечатков пальцев
  • Служба аттестации
  • Репозиторий сотрудников
  • Репозиторий в Интернете
  • Пользовательский интерфейс
  • Контроллер рабочего процесса посещаемости
  • Подпись фингерпринта

В следующем коде перечисляются некоторые аспекты принципов проектирования:

  • SRP. Одна организация несет ответственность за одно задание.
  • LoD - Закон Деметры - Только поговорите с вашими ближайшими друзьями. Вы заметите, что контроллер не знает ничего о репозиториях.
  • 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();
}

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

Ответ 5

Конечно, процедурное программирование намного проще для людей, которые привыкли писать код процедурно. Для тех, кто используется для написания объектно-ориентированного кода с учетом фактов, процедурный код на самом деле сложнее.

Да, хорошо продуманный объектно-ориентированный код часто приводит к большей работе и более актуальному коду. Но если все сделано правильно, это упрощает управление кодом, облегчает его расширение, облегчает отладку (и, что более важно, легче тестировать).