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

Я видел, как многие компьютерные программисты гордо заявляли: «Ага, я разработал свой код объектно-ориентированным способом - я определил свои элементы данных как частные, к ним можно получить доступ только через общедоступные методы - я создал базовые классы и подклассы, следующие за иерархия - я создал виртуальные методы в базовом классе, которые имеют общее поведение, которое можно использовать в производных классах ». Это и есть объектная ориентация? Определенно есть еще кое-что.

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

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

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

Как тренироваться?

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

Как определить, что мой код - отстой?

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

  1. Добавление дополнительных функций или обязанностей к классу, которые не связаны между собой. Рассмотрим приведенный ниже пример
class Account
{    
   public void Withdraw(decimal amount)    
   {        
      try        
      {            
          // logic to withdraw money       
      }        
      catch (Exception ex)        
      {            
          System.IO.File.WriteAllText(@"c:\logs.txt",ex.ToString());   
      }    
   }
}

Можете ли вы понять проблему в приведенном выше коде? Класс учетной записи взял на себя дополнительную ответственность за добавление исключений в файл журнала. В будущем, если возникнет необходимость изменить механизм ведения журнала, необходимо изменить класс Account. Ну, похоже, это не так.

Исправление:

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

public class Logger
{
   public void Handle(string error)
   {
       System.IO.File.WriteAllText(@"c:\logs.txt", error);
   }
 }

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

class Account
{
   private Logger obj = new Logger();    
   public void Withdraw(decimal amount)    
   {        
      try        
      {            
          // logic to withdraw money       
      }        
      catch (Exception ex)        
      {            
          obj.Handle(ex.ToString());
      }    
   }
}

2. Учтите, что появилось новое требование для работы с разными типами счетов, такими как сберегательный и текущий счет. Чтобы удовлетворить это, я добавляю свойство в класс Account под названием «AccountType». В зависимости от типа счета различается и процентная ставка. Я пишу метод расчета процентов в зависимости от типа счета.

class Account
{
   private int _accountType;

   public int AccountType
   {
      get { return _accountType; }
      set { _accountType = value; }
   } 
   public decimal CalculateInterest(decimal amount)    
   {        
      if (_accountType == "SavingsAccount")
      {
         return (amount/100) * 3;
      }
      else if (_accountType == "CurrentAccount")
      {
         return (amount/100) * 2;
      }
   }
}

Проблема с приведенным выше кодом - «разветвление». Если в будущем появится новый тип учетной записи, снова необходимо изменить класс учетной записи, чтобы удовлетворить требованиям, которые линейно увеличивают количество условий if-else в коде. Обратите внимание, что мы меняем класс Account при каждом изменении. Это явно не расширяемый код.

Исправление:

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

public class Account
{
   public virtual decimal CalculateInterest(decimal amount)
   {
       // default 0.5% interest
       return (amount/100) * 0.5;
   }
}
public class SavingsAccount: Account
{
   public override decimal CalculateInterest(decimal amount)
   {
       return (amount/100) * 3;
   }
}
public class CurrentAccount: Account
{
   public override decimal CalculateInterest(decimal amount)
   {
       return (amount/100) * 2;
   }
}

3. Оставайтесь со мной, с этого момента это становится еще интереснее. Предположим, компания предлагает новый тип учетной записи под названием «Онлайн-учетная запись». Этот счет имеет хорошую процентную ставку 5% и имеет все преимущества, аналогичные сберегательному счету. Однако вы не можете снять наличные в банкомате. Вы можете переводить деньги только банковским переводом. Реализация выглядит так, как показано ниже

public class Account
{
   public virtual void Withdraw(decimal amount)
   {
       // base logic to withdraw money
   }
   public virtual decimal CalculateInterest(decimal amount)
   {
       // default 0.5% interest
       return (amount/100) * 0.5;
   }
}
public class SavingsAccount: Account
{
   public override void Withdraw(decimal amount)
   {
        // logic to withdraw money
   }
   public override decimal CalculateInterest(decimal amount)
   {
       return (amount/100) * 3;
   }
}
public class OnlineAccount: Account
{
   public override void Withdraw(decimal amount)
   {
        throw new Exception("Not allowed");
   }
   public override decimal CalculateInterest(decimal amount)
   {
       return (amount/100) * 5;
   }
}

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

public void CloseAllAccounts()
{
    // Retrieves all accounts related to this customer
    List<Account> accounts = customer.GetAllAccounts();
    foreach(Account acc in accounts)
    {
      // Exception occurs here
      acc.Withdraw(acc.TotalBalance);
    }
}

Согласно иерархии наследования объект Account может указывать на любой из своих дочерних объектов. Во время компиляции не наблюдается необычного поведения. Однако во время выполнения он выдает исключение «Не разрешено». Что мы из этого сделали? Родительский объект не может полностью заменить дочерний объект.

Исправление:

Давайте создадим 2 интерфейса: один для обработки процентов (IProcessInterest), а другой - для обработки вывода (IWithdrawable).

interface IProcessInterest
{
    decimal CalculateInterest(decimal amount);
}
interface IWithdrawable
{
    void Withdraw(double amount);
}

Класс OnlineAccount будет реализовывать только IProcessInterest, тогда как класс Account будет реализовывать как IProcessInterest, так и IWithdrawable.

public class OnlineAccount: IProcessInterest
{
    public decimal CalculateInterest(decimal amount)
    {
       return (amount/100) * 5;
    }
}
public class Account: IProcessInterest, IWithdrawable
{
   public virtual void Withdraw(decimal amount)
   {
       // base logic to withdraw money
   }
   public virtual decimal CalculateInterest(decimal amount)
   {
       // default 0.5% interest
       return (amount/100) * 0.5;
   }
}

Теперь это выглядит чистым. мы можем создать Список ‹IWithdrawable› и добавлять в него соответствующие классы. В случае, если допущена ошибка при добавлении OnlineAccount в список внутри метода GetAllAccounts, мы получим ошибку времени компиляции.

public void CloseAllAccounts()
{
    // Retrieves all withdrawable accounts related to this customer
    List<IWithdrawable> accounts = customer.GetAllAccounts();
    foreach(Account acc in accounts)
    {
      acc.Withdraw(acc.TotalBalance);
    }
}

4. Предположим, что наш класс Account очень востребован. Бизнес предлагает идею выставить API, который позволяет снимать деньги в банкоматах сторонних банков. Мы открыли веб-службу, и все остальные банки начали использовать веб-службу для снятия денег. До сих пор все звучит хорошо. Через пару месяцев компания выдвигает еще одно требование, в котором говорится, что некоторые другие банки также просят установить лимит снятия средств с этого счета в их банкоматах. Нет проблем, подайте заявку на изменение - это легко сделать.

interface IWithdrawable
{
   void Withdraw(decimal amount);
   void SetLimit(decimal limit);
}

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

Исправление:

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

interface IExtendedWithdrawable: IWithdrawable
{
   void SetLimit(decimal limit);
}

Таким образом, старые клиенты будут продолжать использовать IWithdrawable, а новые клиенты смогут использовать IExtendedWithdrawable. Простой, но эффективный!

5. Вернемся к первой проблеме, которую мы решили, когда мы добавили класс Logger, чтобы делегировать ответственность за ведение журнала из класса Account. Он регистрирует исключения в файле. Иногда файлы журналов легко получить по электронной почте или интегрировать с некоторыми сторонними программами просмотра журналов для облегчения доступа. Давайте реализуем это.

interface ILogger
{
    void Handle(string error);
}
public class FileLogger : ILogger
{
   public void Handle(string error)
   {
       System.IO.File.WriteAllText(@"c:\logs.txt", error);
   }
}
public class EmailLogger: ILogger
{
   public void Handle(string error)
   {
       // send email
   }
}
public class IntuitiveLogger: ILogger
{
   public void Handle(string error)
   {
       // send to third party interface
   }
}
class Account : IProcessInterest, IWithdrawable
{
   private ILogger obj;    
   public void Withdraw(decimal amount)    
   {        
      try        
      {            
          // logic to withdraw money       
      }        
      catch (Exception ex)        
      {            
          if (ExType == "File")
          {
             obj = new FileLogger();
          }
          else if(ExType == "Email")
          {
             obj = new EmailLogger();
          }
          obj.Handle(ex.Message.ToString());
      }    
   }
}

Мы написали отличный расширяемый код - ILogger, который действует как общий интерфейс для других механизмов журналирования. Однако мы снова нарушаем класс Account, давая больше ответственности решать, какой экземпляр класса Logging нужно создать. В задачу класса Account не входит принятие решения о создании объекта для ведения журнала.

Исправление:

Решение состоит в том, чтобы «Инвертировать зависимость» к какому-то другому классу, а не к классу Account. Давайте внедрим зависимость через конструктор класса Account. Таким образом, мы возлагаем ответственность на клиента за то, чтобы решить, какой механизм ведения журнала необходимо применить. Клиент, в свою очередь, может зависеть от параметров конфигурации приложения. По крайней мере, в этой статье мы можем не беспокоиться о понимании уровня конфигурации.

class Account : IProcessInterest, IWithdrawable
{
   private ILogger obj;
   public Customer(ILogger logger)
   {
       obj = logger;
   }
}
// Client side code
IWithdrawable account = new SavingsAccount(new FileLogger());

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

Если вы, как и я, верите, что объектно-ориентированное программирование - это сдвиг в мышлении, пожалуйста, нажмите ❤️ сердце внизу , чтобы порекомендовать эту статью, чтобы ее могли увидеть другие. .