четверг, 27 января 2011 г.

Немного поупражнялся в рефакторинге

Пару дней назад увидел пост на крайне актуальную тему в одном занимательном блоге. Суть: автор увидел кусок говнокода на Scala, переписал в цивильном виде, сделал аналог переписанного на Java и сравнил. По итогам сомнения он пришёл к выводу что разница в 30 строк кода из 100 не существенна для выбора языка, а идейной разницы никакой нет.

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

Во-первых, утверждается, что два текста программ, приводимых в конце, одинаковы. И один имеет не лучшую надёжность, чем другой. Это просто не верно. Дело в том, что несмотря на очень похожую структуру Scala-код имеет на несколько порядков большую "защиту от дурака". Для обеспечения сравнимого уровня пассивной защиты от ошибок на Java требовалось-бы написать как-то так:
@rest.Method(httpMethod = Array(POST)) 
public void login(final rest.Request request, 
      @rest.Param(name = "login") @Nullable final String login, 
      @rest.Param(name = "password") @Nullable final String password) 
{ 
   debug("accessing login... " + login); 

   if( login == null || password == null ) 
      respond(UNAUTHORIZED, "You should provide login and password"); 
   else 
      doLogin(request, login, password); 
} 

private void doLogin(@NotNull final rest.Request request, @NotNull final String login, @NotNull final String password) 
{ 
   final UserInfo user = AccountsStorage.find(login); 
   if( user == null ) 
      handleUnknownUserLogin(); 
   else 
      (new KnownUserLoginHandler(request, login, password, user)).handle(); 
} 
И прикрутить сверху проверку статическим анализатором кода. Говорить о читабельности кода после такого преобразования не приходится.

Во-вторых приводится аргумент, что разработчик, с учётом возможностей современных IDE будет руками писать кода Java ничуть не больше(Справедливости ради стоит заметить, что скорее всего даже меньше). Аргумент не очень хороший, в контексте того что код ведь обычно не столько пишут, сколько читают. У меня на строку написанного кода приходится минимум сотня прочитанного, и с пять сотен бегло просмотренного. Во время работы каждый программист возвращается к ранее сделанному много раз и шум вроде повторения списка аттрибутов класс четыре раза в разных обрамлениях мягко говоря не помогает понять свою или чужую мысль.

В-третьих, в примере используется внешняя библиотека (для работы с http судя по всему) совершенно не приспособленная к языку и не использующая его возможностей. Как на образец удачной, основанной на полноценном использовании языка библиотеки можно посмотреть например на Circumflex Web Framework.

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

Итак, что мне не нравится в предложенном решении и что я собираюсь улучшить:
  1. Сквозная связанность всего кода авторизации с классами внешней библиотеки. Не то чтобы переезды целых приложений с одного фреймворка на другой являются обычным делом, но когда они всё-таки происходят...
  2. Весь алгоритм по сути выполняет простой выбор: авторизовать пользователя или выдать сообщение о том, почему в авторизации отказано. В предложенном решении для того чтобы это понять требуется отсмотреть всю сотню строк кода, а исключить другие (ошибочные) варианты реакции весьма затруднительно.
  3. Сам алгоритм авторизации имеет преимущественно линейную структуру:
    • проверить, что логин и пароль предоставлены;
    • найти соответствующие учётные данные в БД;
    • проверить, что пользователь имеет право на вход;
    • аутентифицировать по паролю.
    Несмотря на это код с самого начала начинает ветвиться, полностью скрывая основную идею.

Приступим. Вся процедура авторизации имеет своим результатом или учётные данные авторизованного пользователя, или сообщение об ошибке. Так и запишем:
type LoginResult = Either[UserInfo, String]
def Success[T, E] = Left[T, E] _
def Failure[T, E] = Right[T, E] _

Сама процедура логина получает возможно предоставленные данные, и возвращает описанный выше результат:
def login(login: Option[String], password: Option[String]): LoginResult = ...

Теперь можно отвлечься от деталей процедуры логина и заняться обработкой результатов
@rest.Method(httpMethod = Array(POST))
def login(request: rest.Request,
    @rest.Param(name = "login")
    loginParam: String,
    @rest.Param(name = "password")
    passwordParam: String): Unit =
  login(Option(loginParam), Option(passwordParam)) match {
    case Left(user) =>
      request.session("user") = user
    case Right(message) =>
      respond(UNAUTHORIZED, message)
  }
Здесь важно во-первых что количество вариантов внешней реакции системы явно ограничивается. Во вторых, что на этом уровне заканчиваются все связи с фреймворком, включая возможные null'ы в параметрах вызова (именно по этому я не стал делать процедуры, аналогичной setupSuccessfulAuthResult - она просто не создавала бы никакой полезной абстракции).

Дальше хочется заняться собственно процедурой логина, но стоит снова оставить её в стороне и описать все шаги авторизации. Очевидно, что каждый из шагов будет иметь то-же результат, что и процедура логина в целом, однако будет требовать разных данных на вход. Можно записать каждый шаг в виде отдельной функции:
def findUser(login: String): LoginResult =
  AccountsStorage.find(login).toLeft( "User not found" )

def checkUser(user: UserInfo): LoginResult =
  if (user.inactive) Failure("Account is inactive")
  else Success(user)

def doLogin(user: UserInfo, login: String, password: String): LoginResult =
  if (user.authScheme == "PETRIVKA")
    handlePetrivkaAuthSchemeLogin(user, password)
  else
    handleUsualAuthSchemeLogin(user, login, password)

def handlePetrivkaAuthSchemeLogin(user: UserInfo, password: String): LoginResult =
  if( user.passwordMatches(password) ) Success(user)
  else Failure("Authentication failed")

def handleUsualAuthSchemeLogin(user: UserInfo, login: String, password: String) =
  AccessStorage.access.auth_configs.find(_.key == user.authScheme) match {
    case Some(scheme) =>
      log.debug("authenticating with " + scheme.command)
      val exec = Runtime.getRuntime.exec(
          scheme.command replace("{login}", login) replace("{password}", password))
      if( exec.waitFor == 0 )
        Success(user)
      else
        Failure("Authentication within " + scheme + " failed")
    case None => Failure("Unknown authentication scheme: " + user.authScheme)
  }
Если немного помедитировать на handleUsualAuthSchemeLogin, то наверняка можно её сократить и упростить, но это мало повлияет на основную идею решения.

Теперь осталось самое простое - собрать все шаги вместе. Совершенно случайно в Scala завалялась подходящая конструкция :)
def login(login: Option[String], password: Option[String]): LoginResult =
  for (login <- login.toLeft( "You should provide login" ).left;
       password <- password.toLeft( "You should provide password" ).left;
       user <- findUser(login).left;
       checkedUser <- checkUser(user).left;
       loggedUser <- doLogin(checkedUser, login, password).left
  ) yield loggedUser
Не вдаваясь в детали, скажу что выражение for делает очередной шаг и проверяет результат: если он успешен то продолжает цепочку, если неуспешен то прерывает цепочку, возвращая неудачу. То какой вариант сейчас считается успешным мы сообщаем в конце каждого выражения, я соответственно всегда считаю успешным левый. Метод toLeft у класса Option преобразует его в Either, говоря куда помещать существующее значение и чем заменять несуществующее.

Вот и всё. Данный пример исправляет отмеченные ранее недостатки, при этом имеет более простую (7 элементов против 9) и близкую к задаче структуру. Также стоит добавть, что он имеет и существенный недостаток: двойную терминологию. В одних местах используется пара Success-Failure, в других Left-Right. Однако это имеет и положительный эффект - интерпретация результатов всегда отличима от их создания.

Вот полный исходник с кое-какими моками для компилябельности.
class LoginDemo {

  type LoginResult = Either[UserInfo, String]
  def Success[T, E] = Left[T, E] _
  def Failure[T, E] = Right[T, E] _

  //@rest.Method(httpMethod = Array(POST))
  def login(request: rest.Request,
      //@rest.Param(name = "login")
      loginParam: String,
      //@rest.Param(name = "password")
      passwordParam: String): Unit =
    login(Option(loginParam), Option(passwordParam)) match {
      case Left(user) =>
        request.session("user") = user
      case Right(message) =>
        respond(UNAUTHORIZED, message)
    }

  def login(login: Option[String], password: Option[String]): LoginResult =
    for (login <- login.toLeft( "You should provide login" ).left;
         password <- password.toLeft( "You should provide password" ).left;
         user <- findUser(login).left;
         checkedUser <- checkUser(user).left;
         loggedUser <- doLogin(checkedUser, login, password).left
    ) yield loggedUser

  def findUser(login: String): LoginResult =
    AccountsStorage.find(login).toLeft( "User not found" )

  def checkUser(user: UserInfo): LoginResult =
    if (user.inactive) Failure("Account is inactive")
    else Success(user)

  def doLogin(user: UserInfo, login: String, password: String): LoginResult =
    if (user.authScheme == "PETRIVKA")
      handlePetrivkaAuthSchemeLogin(user, password)
    else
      handleUsualAuthSchemeLogin(user, login, password)

  def handlePetrivkaAuthSchemeLogin(user: UserInfo, password: String): LoginResult =
    if( user.passwordMatches(password) ) Success(user)
    else Failure("Authentication failed")

  def handleUsualAuthSchemeLogin(user: UserInfo, login: String, password: String) =
    AccessStorage.access.auth_configs.find(_.key == user.authScheme) match {
      case Some(scheme) =>
        //log.debug("authenticating with " + scheme.command)
        val exec = Runtime.getRuntime.exec(
            scheme.command replace("{login}", login) replace("{password}", password))
        if( exec.waitFor == 0 )
          Success(user)
        else
          Failure("Authentication within " + scheme + " failed")
      case None => Failure("Unknown authentication scheme: " + user.authScheme)
    }

  def respond(code: Int, message:String = "") = {}

  val UNAUTHORIZED = 401
}

class UserInfo {
  val inactive = false
  val authScheme = ""

  def passwordMatches(pwd: String) = true
}

object AccountsStorage {
  def find(login: String): Option[UserInfo] = None
}

object AccessStorage {
  object access {
    object auth_configs {
      def find(pred: {val key: String} => Boolean): Option[{val command: String}] = None
    }
  }
}

package rest {
  class Request {
    val session = scala.collection.mutable.Map[String, Any]()
  }
}

четверг, 13 января 2011 г.

Провёл семинар по Java

В начале декабря проводил семинар для пары студентов из OSLL про Java. Так как народ в основном ковыряет железки больше что-то не нашлось кому рассказать у них про Java. Вот я и вызвался. Ораторские неспособности потренировать, да для себя какой-нибудь забавный фактик отрыть.

Идея провести семинар (точнее взяться наконец и провести, ибо тема семинара уже давно в их виш-листе висела) пришла ко мне давно. Соответственно кое какие мысленные подготовительные этапы я прошёл. Хотелось поменьше рассказать про язык (до него благо хоть от ++ хоть от # полшага сделать) и по больше про виртуальную машину, библиотеки, то что Sun при жизни именовал экосистемой.

Наученный горьким опытом недавней подготовки лекции про RDF для семинаров лаборатории (там была масса "ощущений" и последующих выводов, надо бы тоже отписаться) я начал готовиться за 3 недели. Сел, бодро накидал план в 4 пункта:

  1. Устройство платформы. Ну понятно VM, байт-код, компилятор, библиотеки.
  2. Пример простой программы. Тут помимо куска кода хотелось ещё скомпилировать и потом рассмотреть декомпилятором что получилось. Основная цель - изгнать из сознания слушателей мысли о волшебстве в байт-коде и работе JVM.
  3. Обзор языка. Быстро, чтобы только понять о чём речь. Класы, интерфейсы, методы и аттрибуты, примитивные типы.
  4. Собственно обзор "мира". Библиотеки, фреймворки, ну и моё маленькое увлечение - другие языки.

Первый и второй пункты написал быстро, вечера за три. Дальше стало резко хуже. Когда что-то используешь особо не задумываешься. Но когда надо заключить это в текстом и потом рассказать свежему человеку понимаешь насколько там на самом дохрена всего. Чуть больше года назад я писал небольшую вводную статья о Scala на хабр, там было и правда легко - язык я знал чуть, "особых случаев" там поменьше, да и в глаза они особо не бросались. А в Java ведь так и не знаешь с какого конца браться. "Пишем в класса public static void main" - какой класс? Откуда он взялся? Откуда не начни приходишь к класслоадерам и мониторам. Ладно, кое как разложил, упорядочил, докинул в начальный план дженериков, а то как-то совсем убого выглядела старушка.

Ещё был неприятный вопрос с примерами. С одной стороны рассказывать о конструкциях языка махая в воздухе руками как-то странно. С другой стороны пространные синтаксические конструкции не располагают к выписыванию и запоминанию. Кроме того привод пример программы надо как-то объяснять что она делает. В общем покрутив и так и сяк план, понял, что стоит вернуться к классической схеме. А именно сначала описание языка и синтаксических конструкций с кусочками кода (как ни старался не получается их делать содержательными). Потом пример, декомпиляция. В итоге 2 и 3 пункт поменялись местами.

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

Последним кусочком был обзор мира, начальный план был обширен:

  1. Бибилиотеки/фреймворки
    1. JEE
    2. Spring
    3. Apache-commons
    4. OSGi
  2. Инструменты
    1. Ant
    2. Maven
    3. IDEs (Eclipse, IDEA, Neteans)
  3. Языки
    1. Groovy
    2. Scala
    3. Clojure

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

Ну затем и пришёл день события. Из человек 6 отписавшихся о желании слушать по факту пришло 2 :) Я про себя от души посмеялся, но всё-таки количество народу полностью развязало мне руки в плане скорости изложения и объёма. Надо заметить, что оба имели хорошую вводную по Java и кажется уже изрядно программировали на ++. Так что я как мог навалился на рассказ про архитектуру платформы. Язык прошёл легко, большая часть вещей была народу знкома, действуя по ситуации, я пару раз заворачивал вглубь: немного сказал про ограничения в дженериках(<T extends Iterable>) и анонимные классы. Явный промах был только в части обзоров. Уже в процессе я понял, что идея рассказывать про библиотеки без юзкейсов и примеров кода - порочна по определению. Однако даже этот кусочек не совсем пропал - идея Inversion of control кажется всё-таки нашла понимание в массах. Ну и естестенно я как мог с запалом рассказал про Groovy и Scala, перечислил длинный список плюшек. По Groovy набросал примеров на поиски и выборки из коллекций. Из Scala привёл пример quicksort а-ля Haskell (поставил я тут, кстати, эксперимент на сортировку 1 000 000 интов - всего в три раза медленнее нативного на массивах, кажется в Швейцарских университетах практикуют чёрную магию). Ну и вычисление ряда Фибоначи на скобочках естественно =)

Ну и напоследок я спросил нужно ли продолжение и ответ был положительным. Так что постараюсь по весне сделать либо что-то поглубже про Java (это если Блох приедет и успею прочитать) либо (что привлекает больше и будет иметь больше практического фундамента к тому времени) краткое введение в Groovy и Scala. Ибо как говорит один английский профессор - "лучший способ разобраться в чём-нибудь - прочиать по этому курс".

PS вот моя недопрезентация к семинару, в основном веслые картинки: http://dl.dropbox.com/u/1776995/pres.pdf