Статьи

Еще один аспект взаимодействия в объектно-ориентированной парадигме

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

В этой статье я хотел бы пролить свет на тесную зависимость от типа используемого компонента. Как правило, мы стремимся разрабатывать классы так, чтобы они взаимодействовали через интерфейсы или, в более общем случае, создавались через API. Предположим, что мы используем интерфейсы (более общий термин, в котором у нас нет реализаций, а не только для интерфейса с ключевыми словами в Java или C #), но этого недостаточно, нам нужно предоставить некоторую реализацию для интерфейса, который фактически используется другие клиентские классы.

Прежде чем углубляться в детали, позвольте мне привести несколько примеров (примеров на Java): я хотел бы разработать Reader, который помог бы клиентским классам получать информацию из любого указанного источника, будь то Файловая система / Web. Таким образом, интерфейс будет:

1
2
3
4
5
6
7
8
9
interface Reader
{
  public String read();
  
  /**
   * Get the source to read from
   */
  public String getSource();
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class FileSystemReader implements Reader
{
  private String source;
  public FileSystemReader(String source)
  {
    this.source = source;
  }
  @override
  public String getSource()
  {
    return this.source;
  }
  public String read()
  {
    //Read from the source.
    //The source is a file in file system.
  }
}
class HttpReader implements Reader
{
  private String source;
  public FileSystemReader(String source)
  {
    this.source = source;
  }
  @override
  public String getSource()
  {
    return this.source;
  }
  public String read()
  {
    //Read from the source.
    //The source is a document in the web.
  }
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
class Client
{
  /**
   * This is the consumer of the Reader API
   * @param source Source to read from
   */
  public void performSomeOperation(String source)
  {
    Reader myReader = null;
    if(source contains "http://")
    {
      //its a web document, create a HttpReader
      myReader = new HttpReader(source);
    }
    else
    {
      myReader = new FileReader(source);
    }
    print(myReader.read());
  }
}

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

Кроме того, ваш клиентский класс знает, что для этого используется HttpReader, а для этого FileReader, поэтому вы отдаете информацию, относящуюся к типу, для используемого вами экземпляра, и, таким образом, код клиента тесно связан с этой информацией о типе. Этот подход может иногда нарушать принцип Open Closed, потому что вы заканчиваете редактированием класса каждый раз, когда добавляется новая реализация интерфейса. Таким образом, должно быть так или иначе мы можем защитить это создание экземпляров различных реализаций интерфейса от пользователя этих интерфейсов. Да, есть способы, и я знаю, что вы, должно быть, уже ждали, чтобы раскрыть шаблон Factory Method .

Так как же можно изменить приведенный выше код для использования Factory

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * Factory to get the instance of Reader implementation
 */
class ReaderFactory
{
  public static getReader(String source)
  {
    if( source contains http)
    {
      return new HttpReader(source);
    }
    else if(someother condition)
    {
      return new SomeReader(source);
    }
    else
    {
      return new FileReader(source);
    }
  }
}
  
class Client
{
  /**
   * This is the consumer of the Reader API
   * @param source Source to read from
   */
  public void performSomeOperation(String source)
  {
    Reader myReader = ReaderFacotry.getReader(source);
    print(myReader.read());
  }
}

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

Следует иметь в виду, что инкапсуляция — это не только скрытие данных, но и скрытие информации, связанной с типом, от пользователя.

Ссылка: еще один аспект взаимодействия в объектно-ориентированной парадигме от нашего партнера по JCG