SOLID, твердые принципы объектно-ориентированного программирования на C #

SOLID, твердые принципы Программирование и разработка

Принципы SOLID — это набор золотых правил, используемых объектно-ориентированными разработчиками с начала 2000-х годов. Они установили стандарт программирования на языках ООП, а теперь и в гибкой разработке и многом другом. Программы SOLID лучше масштабируются, требуют меньше времени для работы и могут легче реагировать на изменения. Работодатели всегда предпочтут кандидата, хорошо разбирающегося в принципах SOLID.

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

Что такое принципы SOLID?

ТВЕРДЫЙ являются мнемоническими устройствами для 5 принципов проектирования в объектно-ориентированных программах (ООП), которые в результате читаемом, адаптируемой и масштабируемой коде. SOLID можно применить к любой программе ООП.

5 принципов SOLID:

  • S принцип Ingle-ответственность
  • О пере закрыто принцип
  • Иськов принцип замещения
  • I принцип сегрегации nterface
  • D принцип инверсии ependency

Принципы SOLID были разработаны преподавателем информатики и писателем Робертом К. Мартином (иногда называемым «дядей Боб») в 2000 году и быстро стали основой современного объектно-ориентированного проектирования (ООД). Аббревиатура SOLID стала обычным явлением, когда эти принципы получили широкую популярность в мире программирования.

Теперь SOLID применяется как в гибкой разработке, так и в адаптивной разработке программного обеспечения.

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

S : принцип единственной ответственности

принцип единственной ответственности

SRP: разделение класса, отличного от SRP

«У класса должна быть только одна ответственность, то есть только изменения в одной части спецификации программного обеспечения должны иметь возможность влиять на спецификацию класса».

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

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

Мартин объяснил это тем, что «у класса должна быть только одна причина для изменения». Здесь «причина» в том, что мы хотим изменить единственную функциональность, которую преследует этот класс. Если мы не хотим, чтобы эта единственная функция изменялась, мы никогда не изменим этот класс, потому что все компоненты класса должны иметь отношение к этому поведению.

Следовательно, мы могли изменить все классы, кроме одного, в программе, не нарушая исходный класс.

SRP позволяет легко следовать другому уважаемому принципу ООП — инкапсуляции. Данные легко скрыть от пользователя, если все данные и методы задания находятся в одном классе единственной ответственности.

Если вы добавляете методы получения и установки к классу с единственной ответственностью, этот класс соответствует всем критериям инкапсулированного класса.

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

Этот фактор также помогает с удобочитаемостью, поскольку вам нужно только прочитать класс, пока вы не определите его функциональность.

Выполнение

Давайте посмотрим на пример того, как можно применить SRP, чтобы сделать наш RegisterUserкласс более читабельным.

// does not follow SRP
public class RegisterService
{
    public void RegisterUser(string username)
    {
        if (username == "admin")
            throw new InvalidOperationException();

        SqlConnection connection = new SqlConnection();
        connection.Open();
        SqlCommand command = new SqlCommand("INSERT INTO [...]");//Insert user into database. 

        SmtpClient client = new SmtpClient("smtp.myhost.com");
        client.Send(new MailMessage()); //Send a welcome email. 
    }
}

Приведенная выше программа не соответствует SRP, потому что RegisterUserвыполняет три разных задачи: регистрирует пользователя, подключается к базе данных и отправляет электронное письмо.

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

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

Вместо этого нам нужно разделить класс на три конкретных класса, каждый из которых выполняет одну задачу. Вот как наш класс будет выглядеть со всеми остальными заданиями, преобразованными в отдельные классы:

public void RegisterUser(string username)
{
    if (username == "admin")
        throw new InvalidOperationException();

    _userRepository.Insert(...);
    
    _emailService.Send(...);
}

Таким образом достигается SRP, поскольку RegisterUserрегистрируется только пользователь, и единственная причина, по которой он может измениться, — это добавление дополнительных ограничений для имени пользователя. Все остальное поведение сохраняется в программе, но теперь достигается с помощью вызовов userRepositoryи emailService.

O : Принцип открыт-закрыт

Принцип открыт-закрыт

OCP: изменение OldClass вместо расширения

«Программные объекты… должны быть открыты для расширения, но закрыты для модификации».

Это утверждение на первый взгляд кажется противоречивым, поскольку оно просит вас запрограммировать объекты (класс / функция / модуль), чтобы они были как открытыми, так и закрытыми. Принцип открытого-закрытого (OCP) требует сущностей, которые могут быть широко адаптированы, но также остаются неизменными. Это приводит нас к созданию повторяющихся сущностей со специализированным поведением посредством полиморфизма.

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

Наша родительская сущность будет служить абстрактным базовым классом, который можно повторно использовать с добавленными специализациями посредством наследования. Однако исходный объект заблокирован, чтобы программа могла быть как открытой, так и закрытой.

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

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

Выполнение

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

// Does not follow OSP
public double Area(object[] shapes)
{
    double area = 0;
    foreach (var shape in shapes)
    {
        if (shape is Rectangle)
        {
            Rectangle rectangle = (Rectangle) shape;
            area += rectangle.Width*rectangle.Height;
        }
        else
        {
            Circle circle = (Circle)shape;
            area += circle.Radius * circle.Radius * Math.PI;
        }
    }

    return area;
}

public class AreaCalculator
{
    public double Area(Rectangle[] shapes)
    {
        double area = 0;
        foreach (var shape in shapes)
        {
            area += shape.Width*shape.Height;
        }

        return area;
    }
}

Эта программа не следует OSP, потому что Area()она не открыта для расширения и может только обрабатывать Rectangleи Circleформировать. Если мы хотим добавить поддержку Triangle, нам придется изменить метод, чтобы он не был закрыт для модификации.

Мы можем достичь OSP, добавив абстрактный класс, Shapeкоторый наследуют все типы фигур.

public abstract class Shape
{
    public abstract double Area();
}

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    public override double Area()
    {
        return Width*Height;
    }
}

public class Circle : Shape
{
    public double Radius { get; set; }
    public override double Area()
    {
        return Radius*Radius*Math.PI;
    }
}

public double Area(Shape[] shapes)
{
    double area = 0;
    foreach (var shape in shapes)
    {
        area += shape.Area();
    }

    return area;
}

Теперь каждый подтип формы обрабатывает собственное вычисление площади посредством полиморфизма. Это открывает Shapeкласс для расширения, потому что новая форма может быть легко добавлена ​​с ее собственным вычислением площади без ошибок.

Кроме того, ничто в программе не изменяет исходную форму, и ее не нужно будет изменять в будущем. В результате в программе реализован принцип OCP.

Продолжайте изучать ООП на C #

Не останавливайтесь только на ТВЕРДОМ. Освойте все основы ООП на C #.

Интерактивные текстовые курсы Educative обеспечивают длительное обучение навыкам, которые ищет каждый интервьюер по C #.

Изучите объектно-ориентированное программирование на C #

L : Принцип подстановки Лискова

Принцип подстановки Лискова

LSP: бесшовная замена класса B в существующем коде

«Объекты в программе должны быть заменены экземплярами их подтипов без изменения правильности этой программы».

Принцип подстановки Лискова (LSP) — это конкретное определение отношения подтипов, созданное Барбарой Лисков и Жаннетт Винг. Принцип гласит, что любой класс должен быть напрямую заменен любым из своих подклассов без ошибок.

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

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

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

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

Выполнение

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

Этот пример не соответствует LSP:

namespace SOLID_PRINCIPLES.LSP
{
    class Program
    {
        static void Main(string[] args)
        {
            Apple apple = new Orange();
            Debug.WriteLine(apple.GetColor());
        }
    }
    public class Apple
    {
        public virtual string GetColor()
        {
            return "Red";
        }
    }
    public class Orange : Apple
    {
        public override string GetColor()
        {
            return "Orange";
        }
    }
}

Это не следует за LSP, потому что Orangeкласс не может заменить Appleкласс без изменения вывода программы. GetColor()Метод переопределяется Orangeклассом и, следовательно, вернуться, что яблоко оранжевое.

Чтобы изменить это, мы добавим абстрактный класс для Fruitчто как Appleи Orangeбудет осуществлять.

namespace SOLID_PRINCIPLES.LSP
{
    class Program
    {
        static void Main(string[] args)
        {
            Fruit fruit = new Orange();
            Debug.WriteLine(fruit.GetColor());
            fruit = new Apple();
            Debug.WriteLine(fruit.GetColor());
        }
    }
    public abstract class Fruit
    {
        public abstract string GetColor();
    }
    public class Apple : Fruit
    {
        public override string GetColor()
        {
            return "Red";
        }
    }
    public class Orange : Fruit
    {
        public override string GetColor()
        {
            return "Orange";
        }
    }
}

Теперь любой подтип ( Appleили Orange) Fruitкласса можно без ошибок заменить другим подтипом благодаря специфичному для класса поведению GetColor(). В результате в этой программе реализован принцип LSP.

I : Принцип разделения интерфейса

Принцип разделения интерфейса

Интернет-провайдер: наследовать только применимые методы

«Многие клиентские интерфейсы лучше, чем один интерфейс общего назначения».

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

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

Любую неиспользованную часть метода следует удалить или выделить в отдельный метод.

Преимущество ISP в том, что он разбивает большие методы на более мелкие и более конкретные методы. Это упрощает отладку программы по трем причинам:

  1. Между классами переносится меньше кода. Меньше кода — меньше ошибок.
  2. Один метод отвечает за меньшее количество вариантов поведения. Если есть проблема с поведением, вам нужно только просмотреть более мелкие методы.
  3. Если общий метод с несколькими поведениями передается классу, который не поддерживает все варианты поведения (например, вызов свойства, которого нет у класса), возникнет ошибка, если класс попытается использовать неподдерживаемое поведение.

Выполнение

Чтобы увидеть, как принцип ISP выглядит в коде, давайте посмотрим, как программа изменяется с соблюдением принципа ISP и без него.

Во-первых, программа, которая не следует за интернет-провайдером:

// Not following the Interface Segregation Principle  
  
  public interface IWorker  
  {  
      string ID { get; set; }  
      string Name { get; set; }  
      string Email { get; set; }  
      float MonthlySalary { get; set; }  
      float OtherBenefits { get; set; }  
      float HourlyRate { get; set; }  
      float HoursInMonth { get; set; }  
      float CalculateNetSalary();  
      float CalculateWorkedSalary();  
  }  
  
  public class FullTimeEmployee : IWorker  
  {  
      public string ID { get; set; }  
      public string Name { get; set; }  
      public string Email { get; set; }  
      public float MonthlySalary { get; set; }  
      public float OtherBenefits { get; set; }  
      public float HourlyRate { get; set; }  
      public float HoursInMonth { get; set; }  
      public float CalculateNetSalary() => MonthlySalary + OtherBenefits;  
      public float CalculateWorkedSalary() => throw new NotImplementedException();  
  }  
  
  public class ContractEmployee : IWorker  
  {  
      public string ID { get; set; }  
      public string Name { get; set; }  
      public string Email { get; set; }  
      public float MonthlySalary { get; set; }  
      public float OtherBenefits { get; set; }  
      public float HourlyRate { get; set; }  
      public float HoursInMonth { get; set; }  
      public float CalculateNetSalary() => throw new NotImplementedException();  
      public float CalculateWorkedSalary() => HourlyRate * HoursInMonth;  
  }

Эта программа не следует за ISP, потому что FullTimeEmployeeклассу не нужна CalculateWorkedSalary()функция, а ContractEmployeeклассу не нужна CalculateNetSalary().

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

Вот как можно реорганизовать программу в соответствии с принципом ISP:

// Following the Interface Segregation Principle  
  
    public interface IBaseWorker  
    {  
        string ID { get; set; }  
        string Name { get; set; }  
        string Email { get; set; }  
         
         
    }  
  
    public interface IFullTimeWorkerSalary : IBaseWorker  
    {  
        float MonthlySalary { get; set; }  
        float OtherBenefits { get; set; }  
        float CalculateNetSalary();  
    }  
  
    public interface IContractWorkerSalary : IBaseWorker  
    {  
        float HourlyRate { get; set; }  
        float HoursInMonth { get; set; }  
        float CalculateWorkedSalary();  
    }  
  
    public class FullTimeEmployeeFixed : IFullTimeWorkerSalary  
    {  
        public string ID { get; set; }  
        public string Name { get; set; }  
        public string Email { get; set; }  
        public float MonthlySalary { get; set; }  
        public float OtherBenefits { get; set; }  
        public float CalculateNetSalary() => MonthlySalary + OtherBenefits;  
    }  
  
    public class ContractEmployeeFixed : IContractWorkerSalary  
    {  
        public string ID { get; set; }  
        public string Name { get; set; }  
        public string Email { get; set; }  
        public float HourlyRate { get; set; }  
        public float HoursInMonth { get; set; }  
        public float CalculateWorkedSalary() => HourlyRate * HoursInMonth;  
    }  

В этой версии мы разделили общий интерфейс IWorkerна один базовый интерфейс IBaseWorkerи два дочерних интерфейса IFullTimeWorkerSalaryи IContractWorkerSalary.

Общий интерфейс содержит методы, общие для всех рабочих. Дочерние интерфейсы разделяют методы по типам работников, FullTimeпо заработной плате или Contractпочасовой оплате.

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

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

D : Принцип инверсии зависимостей

Принцип инверсии зависимостей

DIP: изменения объектов A и B не повлияют на другие

«Следует полагаться на абстракции, а не на конкреции».

Принцип инверсии зависимостей (DIP) состоит из двух частей:

  1. Модули высокого уровня не должны зависеть от модулей низкого уровня. Вместо этого оба должны зависеть от абстракций (интерфейсов).
  2. Абстракции не должны зависеть от деталей. Детали (например, конкретные реализации) должны зависеть от абстакций.

Первая часть этого принципа полностью изменяет традиционный дизайн программного обеспечения ООП. Без DIP программисты часто создают программы, чтобы иметь компоненты высокого уровня (менее подробные, более абстрактные), явно связанные с низкоуровневыми (конкретными) компонентами для выполнения задач.

DIP разделяет высокоуровневые и низкоуровневые компоненты и вместо этого соединяет их с абстракциями. Компоненты высокого и низкого уровня могут по-прежнему извлекать выгоду друг из друга, но изменение одного не должно напрямую нарушать работу другого.

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

Если вы минимизируете зависимости, изменения будут более локализованными и потребуют меньше усилий для поиска всех затронутых компонентов.

Вторую часть можно представить как «изменение деталей не влияет на абстракцию». Абстракция — это часть программы, обращенная к пользователю.

Детали — это конкретные закулисные реализации, которые делают поведение программы видимым для пользователя. В программе DIP мы могли бы полностью пересмотреть закулисную реализацию того, как программа достигает своего поведения без ведома пользователя.

Этот процесс известен как рефакторинг.

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

Выполнение

Мы создадим общую деловую программу с интерфейсом, высокоуровневыми, низкоуровневыми и детализированными компонентами.

Во-первых, давайте создадим интерфейс с getCustomerName()методом. С этим столкнутся наши пользователи.

public interface ICustomerDataAccess
{
    string GetCustomerName(int id);
}

Теперь мы реализуем детали, которые будут зависеть от ICustomerDataAccessинтерфейса. Таким образом достигается вторая часть принципа DIP.

public class CustomerDataAccess: ICustomerDataAccess
{
    public CustomerDataAccess() {
    }

    public string GetCustomerName(int id) {
        return "Dummy Customer Name";        
    }
}

Теперь мы создадим фабричный класс, который реализует абстрактный интерфейс ICustomerDataAccessи возвращает его в пригодной для использования форме. Возвращенный CustomerDataAccessкласс — это наш низкоуровневый компонент.

public class DataAccessFactory
{
    public static ICustomerDataAccess GetCustomerDataAccessObj() 
    {
        return new CustomerDataAccess();
    }
}

Наконец, мы реализуем высокоуровневый компонент, CustomerBuisnessLogicкоторый также реализует интерфейс ICustomerDataAccess. Обратите внимание, что наш высокоуровневый компонент не реализует наш низкоуровневый компонент, а просто использует его.

public class CustomerBusinessLogic
{
    ICustomerDataAccess _custDataAccess;

    public CustomerBusinessLogic()
    {
        _custDataAccess = DataAccessFactory.GetCustomerDataAccessObj();
    }

    public string GetCustomerName(int id)
    {
        return _custDataAccess.GetCustomerName(id);
    }
}

Вот полная программа как в коде, так и в наглядной диаграмме:

public interface ICustomerDataAccess
{
    string GetCustomerName(int id);
}

public class CustomerDataAccess: ICustomerDataAccess
{
    public CustomerDataAccess() {
    }

    public string GetCustomerName(int id) {
        return "Dummy Customer Name";        
    }
}

public class DataAccessFactory
{
    public static ICustomerDataAccess GetCustomerDataAccessObj() 
    {
        return new CustomerDataAccess();
    }
}

public class CustomerBusinessLogic
{
    ICustomerDataAccess _custDataAccess;

    public CustomerBusinessLogic()
    {
        _custDataAccess = DataAccessFactory.GetCustomerDataAccessObj();
    }

    public string GetCustomerName(int id)
    {
        return _custDataAccess.GetCustomerName(id);
    }
}

Визуальная схема нашей программы доступа к данным

Визуальная схема нашей программы доступа к данным

Читайте также:  Как удалить первый символ из строки Python?
Оцените статью
bestprogrammer.ru
Добавить комментарий