ООП Дизайн: абстрактный дизайн класса против обычного наследования - PullRequest
0 голосов
/ 09 мая 2018

Ради интереса я разрабатываю простую систему хранения данных (виниловых пластинок) и некоторых других общих элементов, связанных с записями (рукава, очистители пластинок и т. Д.).

Поскольку 90% данных будут виниловыми пластинками, а остальные 10% - другими предметами, я подумал о том, чтобы разделить их на два класса: записи и предметы. Имейте в виду, что, хотя в конечном итоге они отличаются друг от друга, продукты имеют определенные общие черты, такие как цена, количество и т.д.

У меня есть сомнения относительно того, должен ли я создавать абстрактный класс, скажем Item, и два класса, расширяющих Item: Records и NonMusicalProducts. Как то так:

abstract class Item {

    static function getItemPrice($itemId, $type='record'){
        $result = 0;
        // query record table if type is record, otherwise query nonmusicalproduct table
        return $result;
    }

    abstract function getItemDetails();
}

Принимая во внимание, что:

class Record extends Item {
    static function getItemDetails($id) {
        // query DB and return record details
    }
}

и NMProduct

class NMProduct extends Item {
    static function getItemDetails($id) {
        // query DB and return NMProduct details
    }
}

Можно ли было бы определить оба метода в NMProduct и Record как статические? Я не всегда получу доступ к этому методу из объекта.

Другой вариант - иметь только класс Item и класс Record, которые наследуются от item, но я пришел к выводу, что он кажется неправильным, особенно при попытке получить подробности:

class Item {
   function getItemDetails($id, $type){
        if ($type == 'record') {
            // query record table using id
        }
        if ($type == 'nmproduct'){
            // query nmproduct table
        }
}

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

Или последний вариант, о котором я могу подумать, это один класс:

class Item {

     function getItemPrice($id, $type) {
         // if type is record then query the record table
         // if type is nmproduct then query the nmproduct table
     }

     function getItemDetails($id, $type) {
         // if type is record then query the record table
         // if type is nmproduct then query the nmproduct table
     }
}

Опять же, этот последний вариант не кажется правильным, так как слишком много несвязанных вещей будет сжато в одном классе. Атрибуты записи (например, artist_id, number_of_discs и т. Д.) Отличаются от nmproduct.

Как лучше всего подойти к этой проблеме?

Ответы [ 5 ]

0 голосов
/ 29 июня 2018

Честно говоря, ваш вопрос, кажется, не совпадает с ООП, а именно с Полиморфизмом.

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

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

Прежде чем сделать это, вам нужно решить, имеет ли это смысл. И запись, и NMProduct - это одно и то же? Я бы на самом деле сказал «да», потому что они оба можно продать в магазине. Item хорошее общее имя? возможно, нет. Но независимо от того, что я думаю, вам нужно сделать последний звонок по этому вопросу.

Если они концептуально не оказываются одним и тем же, возможно, даже наследование не имеет особого смысла. Затем держите их отдельно и дублируйте имена методов. Дело в том, что наследование не должно использоваться только для повторного использования. Наследование следует использовать, если две или более вещи (конкретные классы) являются разными видами одной и той же концептуальной вещи (базовый класс).

С точки зрения доступа к объектам и данным : объекты не должны запрашивать базы данных. Репозитории должны запрашивать базы данных и предоставлять экземпляры сущностей. Сущность должна позволять вам работать с ней независимо от того, как она хранится, извлекается, сохраняется, сериализуется и т. Д. Репозитории должны инкапсулировать часть «КАК».

Обычно, когда у вас есть наследование постоянных объектов (например, сущностей и идеально агрегированных корней - но это выходит за рамки этого обсуждения), у вас также будет аналогичное наследование репозиториев. Таким образом, у вас будет ItemRepository, RecordRepository, полученный из ItemRepository, и NMProductRepository, также полученный из ItemRepository. RecordRepository и NMProductRepository будут извлекать только записи и NMProducts соответственно. ItemRepository извлечет оба и вернет их все вместе как Предметы. Это законно и возможно, потому что и запись, и продукт NMP являются элементом.

Надеюсь, это понятно даже без примеров кода.

0 голосов
/ 29 июня 2018

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

0 голосов
/ 23 июня 2018

Я бы создал 2 класса с различными реализациями getDetails и getPrice:

interface Detailable {
  public function getDetails($id);
  public function getPrice($id);
}

class Record implements Detailable{
  public function getDetails($id) {
    // custom Record Details
  }

  public function getPrice($id) {
    // custom Record price
  }
}


class NMProduct implements Detailable{
  public function getDetails($id) {
    // custom NM Product Details
  }

  public function getPrice($id) {
    // custom NMProduct price
  }
}

И затем передайте экземпляр NMProduct или Record в конструктор для Item:

class Item {

  protected $_type;

  constructor(Detailable $type) {
    $this->_type = $type;
  }

  function getItemPrice($id) {
    return $this->_type->getPrice($id);
  }

  function getItemDetails($id) {
    return $this->_type->getDetails($id);
  }
}

Итак, реальные DB-запросы делегируются от Item конкретной реализации, которая отличается для NMProduct и Record.

0 голосов
/ 27 июня 2018

Лично, что вы хотите иметь в конце дня, это список предметов, независимо от их типа. Таким образом, вы определенно хотите иметь общий базовый класс - это позволит вам обрабатывать все объекты, по крайней мере, базовым способом.

Рассмотрим Generics для этой проблемы, как и в ...

public class Item<T extends IHaveDetails> {
    T details;

    public Price getPrice() {
        return details.getPrice();
    }

    public Details getDetails() {
        return details.getDetails();
    }
}

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

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

Public Class Item { частные данные карты = новая HashMap <> ();

 public Detail getDetail(String name) {
     return details.get(name);
 }

 public void addDetail(Detail detail) {
     return details.put(detail.getName(), detail);
 }

}

Использование вышеприведенного класса будет выглядеть так ...

Item record = new Record();

record.addDetail(new NumericDetail("price", 10.00));
record.getDetail("price");

Удачи!

0 голосов
/ 09 мая 2018

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

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

Первый пример:

abstract class Item
{
  int Id;
  abstract Decimal GetPrice();
  abstract IItemDetail GetDetail();
}

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

Ваш второй пример представляет те неприятные проверки типов, которые упоминались ранее. Они делают ваш код трудным для расширения и трудным для понимания (например, читаемость). Это связано с тем, что, как было сказано ранее, вы переместили специфические для типа операции (которые нельзя обобщить, например, шаблоны) в тип (может быть базовым типом), который не обладает всей информацией, необходимой для выполнения операции. Этот тип ничего не знает об объекте. Ни типа, ни состояния. Перед выполнением операций этот тип должен запросить у цели всю информацию: какой тип объекта? В каком состоянии это? Вот почему мы применяем принцип Tell-Don't-Ask и наследование, чтобы избавиться от этого.

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

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

Но есть больше возможностей для достижения того, что вы пытаетесь сделать. Я сказал Состав раньше. Но как насчет инкапсуляции? Мы могли бы использовать Encapsulation , чтобы извлечь изменяющуюся часть. У нас может быть один тип «Item», который используется для всех видов элементов и извлекать поведение, специфичное для типа (единственное поведение, в котором реализации нашего подтипа будут отличаться, например, доступ к БД в вашем примере) в новый тип:

class Item
{
  // ctor
  Item(IItemPlayer typeSpecificPlayer)
  {
    this.TypeSpecificPlayer = typeSpecificPlayer;
  }

  public void Play()
  {
    this.TypeSpecificPlayer.Play(this);
  }

  public int Id;
  private IItemPlayer TypeSpecificPlayer;
}

interface IItemPlayer 
{
  void Play(Item itemToPlay);
}

class RecordIItemPlayer implements IItemPlayer 
{
  void Play(Item item)  { print("Put the needle to the groove"); }
}

class MovieIItemPlayer implements IItemPlayer
{
  void Play(Item item)  { print("Play the video"); }
}

Конструктор принудительно вводит различные компоненты для Композиции типа "Предмет". В этом примере использовались Composition и Encapsulation . Вы бы использовали это как:

var record = new Item(new RecordPlayer());
var movie = new Item(new MoviePlayer());

recordItem.TypeSpecificPlayer.Play();
movieItem.TypeSpecificPlayer.Play();

Без использования Композиция «IItemPlayer» становится ассоциированным внешним типом:

interface IItemPlayer
{
  void Play(Item item);
}

class RecordIItemPlayer implements IItemPlayer 
{
  void Play(Item item)  { print("Put the needle to the groove"); }
}

class MovieIItemPlayer implements IItemPlayer
{
  void Play(Item item)  { print("Play the video"); }
}

class Item
{    
  int Id;
  Decimal Price;
  IItemDetail Details;
}

и используйте его как:

var record = new Item();
var recordItemPlayer = new RecordItemPlayer();

var movie = new Item();
var movieItemPlayer = new MovieItemPlayer();

recordItemPlayer.Play(record);
movieItemPlayer.Play(movie);

Если нужны дополнительные варианты, например, способы воспроизведения определенного типа медиа, мы бы просто добавили новую реализацию «IItemPlayer».

...