Статьи

Как думать о ОО

Кажется, что все думают, что пишут ОО после того, как они используют ОО-языки, такие как Java, Python или Ruby. Но если вы проверяете код, он часто носит процедурный характер.

Статические Методы

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

Это означает, что для статического метода, чтобы сделать что-то интересное, он должен иметь аргументы. Но в этом случае я буду утверждать, что метод просто принадлежит одному из своих аргументов. Пример: Math.abs (-3) должно быть действительно -3.abs (). Теперь это не означает, что -3 должен быть объектом, только то, что компилятор должен сделать магию от моего имени, что, кстати, Руби понял. Если у вас есть несколько аргументов, вы должны выбрать аргумент, с которым метод взаимодействует больше всего.

Но большинство обоснований для статических методов утверждают, что они являются «служебными методами». Допустим, вы хотите иметь метод toCamelCase () для преобразования строки «my_workspace» в «myWorkspace». Большинство разработчиков решают это как StringUtil.toCamelCase («my_workspace»). Но, опять же, я собираюсь доказать, что метод просто принадлежит классу String и должен быть «my_workspace» .toCamelCase (). Но мы не можем расширить класс String в Java, поэтому мы застряли, но во многих других ОО-языках вы можете добавлять методы к существующим классам.

В конце я иногда (несколько раз в год) вынужден писать статические методы из-за ограниченности языка . Но это редкое событие, так как статические методы — смерть для тестируемости . Я обнаружил, что в большинстве проектов распространены статические методы.

Методы экземпляра

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

class Database {
  // some fields declared here
  boolean isDirty(Cache cache, Object obj) {
    for (Object cachedObj : cache.getObjects) {
      if (cachedObj.equals(obj))
        return false;
    }
    return true;
  }
}

Проблема здесь в том, что метод может быть статичным! Он находится не в том месте, и вы можете сказать это, потому что он не взаимодействует с какими-либо данными в базе данных, а взаимодействует с данными в кеше, которые он выбирает, вызывая метод getObjects (). Я предполагаю, что этот метод относится к одному из его аргументов, скорее всего, кэш. Если вы переместите его в Cache, вы заметите, что для Cache больше не нужен метод getObjects (), поскольку цикл for может напрямую обращаться к внутреннему состоянию Cache. Эй, мы упростили код (переместили один метод, удалили один метод) и порадовали Деметру .

Забавная вещь в методах getter заключается в том, что обычно это означает, что код, в котором обрабатываются данные, находится вне класса, в котором есть данные. Другими словами, код и данные не вместе.

class Authenticator {
  Ldap ldap;
  Cookie login(User user) {
    if (user.isSuperUser()) {
      if ( ldap.auth(user.getUser(),
             user.getPassword()) )
        return new Cookie(user.getActingAsUser());
    } else (user.isAgent) {
        return new Cookie(user.getActingAsUser());
    } else {
      if ( ldap.auth(user.getUser(),
             user.getPassword()) )
        return new Cookie(user.getUser());
    }
    return null;
  }
}

Теперь я не знаю, хорошо ли написан этот код или нет, но я знаю, что метод login () имеет очень высокую привязанность к пользователю. Он взаимодействует с пользователем намного больше, чем с его собственным состоянием. За исключением того, что он не взаимодействует с пользователем, он использует его в качестве хранилища данных. Опять же, код жизни с данными нарушается. Я считаю, что метод должен быть на объекте, с которым он взаимодействует больше всего, в данном случае на пользователе. Итак, давайте посмотрим:

class User {
  String user;
  String password;
  boolean isAgent;
  boolean isSuperUser;
  String actingAsUser;

  Cookie login(Ldap ldap) {
    if (isSuperUser) {
      if ( ldap.auth(user, password) )
        return new Cookie(actingAsUser);
    } else (user.isAgent) {
        return new Cookie(actingAsUser);
    } else {
      if ( ldap.auth(user, password) )
        return new Cookie(user);
    }
    return null;
  }
}

Хорошо, мы делаем успехи, обратите внимание, как исчезла потребность во всех методах получения (и в этом упрощенном примере необходимость в классе Authenticator исчезает), но все равно что-то не так. Ветвь ifs о внутреннем состоянии объекта. Я предполагаю, что эта база кода пронизана if (user.isSuperUser ()). Проблема в том, что если вы добавляете новый флаг, вы должны помнить, чтобы изменить все if, которые разбросаны по всей базе кода. Всякий раз, когда я вижу «Если» или включаю флаг, я почти всегда могу знать, что полиморфизм в порядке.

class User {
  String user;
  String password;

  Cookie login(Ldap ldap) {
    if ( ldap.auth(user, password) )
      return new Cookie(user);
    return null;
  }
}

class SuperUser extends User {
  String actingAsUser;

  Cookie login(Ldap ldap) {
    if ( ldap.auth(user, password) )
      return new Cookie(actingAsUser);
    return null;
  }
}

class AgentUser extends User {
  String actingAsUser;

  Cookie login(Ldap ldap) {
    return new Cookie(actingAsUser);
  }
}

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

Теперь возникает вопрос: должен ли пользователь знать о Ldap? Там на самом деле два вопроса. 1) должен ли пользователь иметь ссылку на поле на Ldap? и 2) должен ли пользователь иметь зависимость времени компиляции от Ldap?

Должен ли пользователь иметь ссылку на поле Ldap? Ответ — нет, потому что вы можете сериализовать пользователя в базу данных, но не хотите сериализовать Ldap. Смотрите здесь .

Должен ли пользователь иметь зависимость времени компиляции от Ldap? Это сложнее, но в общем случае ответ зависит от того, планируете ли вы повторно использовать пользователя в другом проекте, поскольку зависимости времени компиляции переходны в строго типизированных языках. Мой опыт показывает, что все всегда пишут код, что однажды они будут использовать его повторно, но этот день никогда не наступает, и когда это происходит, обычно код запутывается другими способами, так что повторное использование кода после факта просто не происходит. (Разработка библиотеки отличается тем, что повторное использование кода является явной целью.) Я хочу сказать, что многие люди платят цену «что если», но никогда не получают от этого никакой выгоды. Поэтому не беспокойтесь об этом и заставьте пользователя зависеть от Ldap.

С http://misko.hevery.com