ООП-дизайн вокруг абстрактных классов

Допустим, у меня есть класс Namespace, как показано ниже:

abstract class Namespace {
   protected prefix;
   protected Map<String, String> tags;

   public setTag(String tagKey, String tagValue) {
      tags.put(tagKey, tagValue);
   }
}

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

class FooNamespace extends Namespace {
   public String getNameTag() {
      return tags.get("name");
   }
}

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

 class Namespace {
       private prefix;
       private Map<String, String> tags;

       public String getTag(String key) {
          return tags.get(key);
       }
    }

Вышеупомянутое используется как

Namespace.getTag("name");

Ответ во многом зависит от того, чего вы хотите достичь - но если мне придется набрать getTag("name") несколько сотен раз, я обязан: 1: Ненавижу вас и 2: Сделаю ошибку. getNameTag избавляет от некоторых догадок и снижает вероятность того, что я напечатаю "name" неправильно, не заметив этого. Является ли это «хорошим дизайном» - это вопрос мнения и зависит от того, как будет использоваться предполагаемый класс. Достаточно ли распространен "name" в приложении, чтобы быть полезным? Что насчет "даты" или "числовых" значений - там могут быть полезны некоторые вспомогательные методы;)

MadProgrammer 19.03.2018 00:35

Единственное, что меня беспокоит, это то, что, хотя у Bar нет аксессора для «name», он поддерживается абстрактным классом Namespace, который содержит необработанный Map <String, String>, так что любой может вставить тег «name» с помощью выполнение Bar.putTag ("name", "nameValue"); Есть идеи, как реализовать "сеттеры"?

John Baum 19.03.2018 00:40

1. Сделайте поля private вместо protected; 2. Определите interface из Namespace, который обеспечивает неизменяемый вид, и MutableNamespaceinterface, который обеспечивает изменяемую функциональность. Выставляйте interface только на те части приложения, которые в нем нуждаются.

MadProgrammer 19.03.2018 00:48

Спасибо за ответ! Не могли бы вы привести небольшой пример, чтобы я полностью понял?

John Baum 19.03.2018 01:00

«отдельные реализации каждого пространства имен с геттерами, определяющими информацию, извлекаемую из карты «тегов»» - С точки зрения ООП, с использованием геттеров нет хороший дизайн. Нарушает ООП, где объекты раскрывают поведение и инкапсулируют свои потенциальные свойства (геттеры - это не поведения - Зачем вы «получаете»? ЭТО поведение). Вы недостаточно конкретны (пример foo-bar не выражает проблему - это XY). Каковы ваши фактические требования?

Dioxin 19.03.2018 01:32
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
5
119
2

Ответы 2

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

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

Является ли это «хорошим дизайном» - это вопрос мнения и зависит от того, как будет использоваться предполагаемый класс. Достаточно ли распространено «имя» в приложении, чтобы быть полезным? Что насчет "даты" или "числовых" значений - там могут быть полезны некоторые вспомогательные методы;)

The one thing that bugs me about this is that while Bar does not have an accessor for "name", its backed by an abstract Namespace class that contains a raw Map so anyone can shove in a "name" tag by doing a Bar.putTag("name", "nameValue"); Any ideas on how to implement the "setters"?

Это тоже ошибка, которую я испытываю к API коллекций в целом.

Вы можете создавать неизменяемые и изменяемые концепции ...

public interface Namespace {
    public String getTag(String key);
}

public interface MutableNamespace extends Namespace {
    public void setTag(String key, String value);
}

Затем вы можете начать абстрагироваться от этих ...

public abstract class AbstractNamespace implements MutableNamespace {
    private prefix;
    private Map<String, String> tags;

    public setTag(String tagKey, String tagValue) {
        tags.put(tagKey, tagValue);
    }

    public String getTag(String key) {
        return tags.get(key);
    }
}

И, наконец, предоставьте полезные реализации для контекста, в котором он может использоваться ...

public interface MySuperHelpfulNamespace extends Namespace {
    public String getNameTag();
}

public class DefaultMySuperHelpfulNamespace extends AbstractNamespace  implements MySuperHelpfulNamespace {
    public String getNameTag() {
        return tags.get("name");
    }
}

Затем напишите приложение, чтобы поддержать их ...

public void someMethodWhichDoesNotNeedToChangeTheValues(MySuperHelpfulNamespace namespace) {
    //...
}

public void someMethodWhichDoesNeedToChangeTheValues(MutableNamespace namespace) {
    //...
}

По сути, это пример «кодирования интерфейса (а не реализации).

Проблема проистекает из ..

Ваша попытка повторно использовать Namespace через наследование его свойства: Map.


Идея объектной ориентации заключается в том, чтобы ..

Скажите объектам, чтобы они выполняли действия.

What is an Object?

Hiding internal state and requiring all interaction to be performed through an object's methods is known as data encapsulation — a fundamental principle of object-oriented programming.

Object

Когда ClassA расширяет ClassB, подтип ClassA должен наследовать поведение или открытый интерфейс своего супертипа ClassB. Ваш тип Namespace, похоже, не определяет никакого поведения.


Вы можете рассматривать setter или getter как поведение,

Но не дайте себя обмануть. В настоящее время у разработчика нет причин предпочитать ваш тип Namespace простому использованию Map, кроме изменения интерфейса, который они используют, с get на getTag.

Методы getter и setter нарушают ООП. Не стесняйтесь прочитайте обсуждение.


Как должен действительно использоваться Namespace? Какое требование это заполнение? Какое поведение должен определять Namespace?

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

Представьте, что ваше требование было ..

Append an individual tag from a variety of XML tag sets to a StringBuilder that'll be used for rendering.

Вместо того, чтобы делать ..

StringBuilder builder = ...;
Namespace namespace = ...;

builder.append(namespace.getTag("name"));

Namespace может отвечать за добавление тегов это (Namespace является владельцем Map, хранящего теги) для всего, что его запрашивает.

public final class Namespace {
    private final Map<String, String> tags;

    public Namespace(Map<String, String> tags) {
        this.tags = tags;
    }

    // if you REALLY need to add tags after instantiation
    public Namespace addTag(String key, String value) {
        Map<String, String> tags = new HashMap<>(this.tags);
        tags.put(key, value);
        return new Namespace(tags);
    }
    
    // the behavior that fufills the requirement
    public void appendTo(StringBuilder builder, String key) {
        builder.append(tags.get(key));
    }
}

Обратите внимание на поведение Namespace (присоединяется к StringBuilder), а не действует как прокси для Map.

StringBuilder builder = ...;
Namespace namespace = ...;

namespace.appendTo(builder, "name");

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

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

Вы не указали свои требования, но, надеюсь, этот ответ поможет вам определить их (в зависимости от того, как вы используете getter) и реализовать их объектно-ориентированным образом.


Как отметил @MadProgrammer:

if I have to type getTag("name") a few hundred times, I'm bound to make a mistake.

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

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

public final class Bar {
    private final Namespace namespace;

    public Bar(Namespace namespace) {
        this.namespace = namespace;
    }

    public void appendNameTo(StringBuilder builder) {
        namespace.appendTo(builder, "name");
    }
}

Вы можете сказать "Ждать! Я хочу передать Bar туда, где ожидается Namespace!"

Это было бы бессмысленно. Если код полагается на Namespace, не следует ожидать безопасности типов даже в вашем коде. Например:

void doSomething(Namespace namespace) {

 }

Если вы не преобразовали или не объявили appendNameTo в Namespace, у вас в любом случае не будет доступа к каким-либо методам, определенным в Bar. Вы сказали, что у подтипов могут быть разные теги. Это означает, что если вы стремитесь к безопасности типов, все ваши подтипы будут иметь разные общедоступные интерфейсы, поэтому я не расширил Namespace.

Другие вопросы по теме