ラベル インターン の投稿を表示しています。 すべての投稿を表示
ラベル インターン の投稿を表示しています。 すべての投稿を表示

2014年12月16日火曜日

NewsPicks(iOS)の設計思想

NewsPicksチームインターン生の保田です。
主にiOSアプリ開発のお手伝いをしております。
iOSアプリを作っていて悩ましいな、と思うのが、同じ機能を持つがiPadとiPhoneで見た目が違うページをどう設計していくか、ということです。

ちょうど僕が関わった、NewsPicksの特徴的な機能である「ニュース中間ページ」をどのように設計したかについてお届けいたします。



NewsPicks上でニュースをみるとき、上図の左から右の流れで画面を切り替えます。
この中の真ん中の画面(ニュース中間ページ)の設計についてご説明していきます。


Season1. 単一仕様時代

当初は、次のような単純な仕様でした
 - 「フォローしているユーザー」セクションで、コメントを表示
 - 「その他のユーザー」セクションで、コメントを表示


ただし、並び順に関してはサーバー側で『時系列順』『Like順』でソートされたものを受信して、そのまま表示していました。

当時は次のようなクラス図になっていました(メソッドは省略)。


ControllerにはiPhone, iPadで共通の処理が双方に書かれていたため、メンテナンス性が低いものになっていました。また、NewsTableViewにはコメントが表示されるのですが、コメントの有無の判定、データの保持などの処理も行っており、ViewとLogicが切り分けられていない典型的なアンチパターンになってしまっていました。


Season2. 人気コメントの誕生

次に、人気ニュース一覧からニュース中間ページに遷移した場合、Likeがたくさん付いているコメントを「人気コメント」というセクションで上位に表示するという仕様が産声を上げました。


仕様を整理すると、下記のようになります。
そこで、以下のような設計に変更しました。


Commentsというクラスを作成し、NewsTableViewから表示するコメントの表示内容、コメントのソートなどを切り離しました。これで、ViewからLogicを少し切り離すことができました。
CommentsWithTrendingCommentsはCommentsを継承した、人気コメントを表示するためのクラスです。NewsTableViewがどうCommentsを生成すれば良いか知っていて、それをControllerが知っているという設計です。実装の詳細を知らないと使えないクラスはイケてません。


Season3. 検索機能の実装と、連載企画のスタート

NewsPicksオリジナル連載企画がはじまり、中間ページに以下のようなページが仲間入りしました。

上のほうに連載名が書いていたり、「記事に登場するユーザー」というカテゴ リが追加されたりしました。
また、検索機能が追加され、コメント検索の結果から遷移すると「ヒットした コメント」というカテゴリを表示することとなりました。
仕様を整理すると、下記になります。
それに伴い大幅な設計変更を行い、以下の様な設計になりました。

大工事です。
5個しか無かったクラスが
20 個近くになりました。緑色のクラスが実際に呼び出されるクラスです。
共通部分は
Abstract に集約し、コメントのロジックは PickerCommentsCategorizeLogic が責務を持 ち 、 データはCategorizedComments で授受されそのまま表示すると正しいセクションが表示されます。
また、
Factory Method パターンを導入したことによって呼び出し側は Abstract など実装の詳細は一切知りません。 しかし、まだ改善の余地があります。『AbstractNewsSummaryController』の 親クラスが iPad, iPhone によって分かれてしまっていて、全く同じ処理が iPhone, iPad のそれぞれに書かれてしまっています。 
Season4. 更なるリファクタリング Season3 で残ってしまった重複コードを削除すべく、リファクタリングを 行いました。iPad だけが継承している TransitionController は、Controller の遷移方法に関する責務を持っていました。これをを Helper として存在さ せ、標準の UIViewController を継承できるようにしました。
こうすることで共通部分を抽象クラスへ追いやることができ、具象クラスの重複コードが撲滅できました。
ここで、season3でFactory Method パターンを導入していたため、呼び出し側では全くコードの変更が必要ありませんでした。GoF さまさまです。
まとめ
NewsPicks では、同じ機能をもつが iPad iPhone で見た目が違うページを、下記のような戦略でメンテナンス性の高い設計で実装しています。 - View, Logic, Controller を切り分ける
- 具象クラスに点在する共通処理を抽象クラスに追いやる
- Factory Method パターンでインスタンスの生成方法を隠すことにより、後々の変更に強くする
サービスローンチ時に、様々な試行錯誤を行いながら徐々にサービスを成長させる過程で、負債コードをためてしまうのは致し方ないことです。しかし、適切な手順で設計を変更すれば必ずコードは綺麗になります!
NewsPicks では更なるグロースに向けて、一緒に戦ってくれるエンジニアを募集しております!
インターン生でも設計から携わらせていただけるのでとても勉強になります!
興味をお持ちいただいた方は Wantedly などからご応募ください!!


2014年12月4日木曜日

Scalaのパーサコンビネータにふれる

技術チームインターンの中村です。

内製化されたシステムを抱えた会社にいると,エンジニア以外の方のためにドメイン特化言語を構築するようなこともあるかと思います。 uzabaseの場合,アナリストがSPEEDAに載せる業界概要の記事を効率良く書けるようになるために,Markdownに似た軽量マークアップ言語が作られました。

作る言語が構文木が不要なほど小規模ならば,文字用ユーティリティだけで十分に言語実装が可能かと思います。 一方で,言語が大規模であったり効率の良いコンパイルが求められたりするのであれば,LexやYaccのようなパーサージェネレータが必要になるかもしれません。

 今回はその間くらい,つまり単純な文字列処理では足りないものの,パーサージェネレータを使うほどでもないくらいの言語を構築するときに便利なScalaのパーサーコンビネータについて紹介します。

受け取った入力の結果を返す関数としてパーサーを組合せることで,目的のパーサーを高階関数(コンビネータ)として構築したものがパーサーコンビネータです。 パーサーは文法構造の連続,繰り返し,選択などを実現する関数で組み合わされます。 このとき,言語が関数あるいはメソッドの中置記法をサポートするなら,パーサーコンビネータの定義はEBNFの生成規則と似たものになります。 たとえば,論理式のEBNFとこれに従う論理式をパースするパーサーコンビネータの対応は以下のようになります。

EBNF
expr ::= termA {"|" termA}
termA ::= termN {"&" termN}
termN ::= factor | {"!" factor}
factor ::= "TRUE" | "FALSE" | "(" expr ")"
|は選択,{}は0以上の繰り返し

パーサーコンビネータ
def expr: Parser[Any] = termA~rep("|"~termA)
def termA: Parser[Any] = termN~rep("&"~termN)
def termN: Parser[Any] = factor | "!"~factor
def factor: Parser[Any] = "TRUE" | "FALSE" | "("~expr~")"
|は選択,~は連続,rep()は0回以上の繰り返し
  
|, ~, repをメソッドとして別のParserを引数にとり,より複雑な文字列を受理するようなParserを返す,この繰り返しによって論理式を受理するパーサを構築しています。 上の定義は宣言的なので,どのような計算がパース過程で行われるのか定義から読み取ることができません。 そこで,アトミックなパーサと,パーサ同士を逐次的に繋げた場合の計算がどのような行われるか見て行きたいと思います。

最小のパーサはsuccess, failureというメソッドです。 これらは入力をまったく消費しません。 2つのメソッドは以下のように定義されています。

def success[T](v: T) = Parser{ in => Success(v, in) }

def failure(msg: String) = Parser { in => Failure(msg, in) }

ヘルパーメソッド
def Parser[T](f: Input => ParseResult[T]): Parser[T] = new Parser[T]{
  def apply(in: Input) = f(in)
}
  
Parserというのが,|や~をメソッドに持つパーサーのクラスです。 Inputは,パーサーが生の文字列だけではなく,トークンのストリームも読めるようにするために,パース対象を抽象化するクラスです。 Success, FailureはParseResultの子クラスとして,それぞれ成功,失敗したパース結果と残りの入力をまとめるクラスです。これらのクラスの宣言は以下のように定義されています。

abstract class Parser[+T] extends (Input => ParseResult[T]) {..}

case class Success[+T](result: T, override val next: Input) extends ParseResult[T] {
  ..
}

case class Failure(override val msg: String, override val next: Input) 
  extends NoSuccess(msg, next) {..}
  
success, failureの次に仕事をするパーサが以下のelemです。 これは,入力を適用すると,入力の先頭がeであれば,resultにe, nextに2番目以降の入力もつSuccessを返します。 また,先頭がeでなければFailureを返します。

def elem(e: Elem): Parser[Elem] = accept(e)
  
ここまでで,Parserが入力(Input)を受けつけ,ヘルパーメソッドのfにinputを適用した結果(result)と入力の残り(next)を返す関数であるようなイメージを持って頂けたと思います。
次に逐次的にパーサ同士をつなげるメソッド~を見ていきます。 ~は以下のように定義されています。

def ~[U](q: => Parser[U]): Parser[~[T, U]] = { lazy val = p = q
  (for {
    a<- this
    b<-p
  } yield new ~(a, b)).named("~")
}
  
new ~(a, b)は繋げられた2つのパーサーの結果をひとつにまとめるためケースクラス~のインスタンスです。 また,for式に必要なflatMap, mapの定義は以下になります。

def flatMap[U](f: T => Parser[U]): Parser[U] = Parser{ in => 
  this(in) flatMapWithNext(f) 
}

def map[U](f: T => U): Parser[U]  = Parser{ in => this(in) map(f)}
  
上の定義で使われるSuccessにおけるflatMapWithNextとmapは以下のようになります。

def flatMapWithNext[U](f: T => Input => ParserResult[U]): ParseResult[U] =
   f(result)(next)

def map[U](f: T => U) = Success(f(result), next)
  
そして,これらを用いてfor式を地道に展開していくと例えば次のように書き直せます。

new Parser[T~U] {
  def apply(in: Input) = Parser.this(in) match {
    case Success(r, n) => p(n) match {
      case Success(r1, n1) => Success(new ~(r, r1), n1)
      case Failure(msg, next) => Failure(msg, next)
    }
    case Failure(msg, next) => Failure(msg, next)
  }
}
  
最初のパーサーが返すParseResultにある残りの入力が次のパーサに渡されることで,計算が進めるようです。 上の定義を見ると,はじめのパーサが失敗すれば計算は終了。 成功すれば,次のパーサpに残りの入力(n)を渡す。 そこで失敗すれば後続する計算がなされず全体の計算が終了。 成功すれば両パーサの計算結果としてケースクラスの~が返されています。

 ここまでで,パーサーコンビネータの実体が引数をパース対象として受付け,関数適用の結果と残りのパース対象の組を返す関数のように振舞うことがなんとなく伝わったかと思います。

 さらに詳しく知りたい方はこちらを参考にパーサーコンビネータそのものを自作してみるとよいかもしれません。 リンク先の資料ではパーサコンビネータがモナド値であることを踏まえた上で実装方法を紹介しています。 事実,ScalaのParserやParseResultはモナド値となっています。
資料ではParserが次のように定義されています。

newtype Parser a = Parser (String -> [(a,String)])
  
InputがString, ParseResultが[(a, String)]とそれぞれ対応しています。 空リストがFailureを意味します。 最後に上のpdfの実装をScalaで試すときに,モナド則の確認やfor式の記述に最低限必要な定義を残しておくので,ご参考にしていただければと思います。

trait Parsers {
  def Parser[A](f: String => List[(A, String)]): Parser[A] = new Parser[A]{
    def apply(input: String) = f(input)
  }

  def success[A](v: A): Parser[A] = Parser{ input => List((v, input)) }

  def failure[A]: Parser[A] = Parser { input => List() }

  trait Parser[A] extends (String => List[(A, String)]) {
    def apply(input: String): List[(A, String)]

    def flatMap[B](f: A => Parser[B]) = Parser{ (input: String) =>
      for {
        (a, input2) <- this(input)
        b <- f(a)(input2)
      } yield b
    }

    def withFilter(pred: A => Boolean): Parser[A] = Parser { input: String =>
      for {
        elm <- this(input) if pred(elm._1)
      } yield elm
    }

    def map[B](f: A => B) = Parser {input =>
      for {
        a <- this(input)
      } yield(f(a._1), a._2)
    }
  }
}
  
付録, for式の展開過程

this.flatMap {
  a => p.map(b => new ~(a, b))
}

Parser {
  in => Parser.this(in) match {
    case Success(r, n) => ((a: T) => p.map(b => new ~(a, b)))(r)(n)
    case Failure(msg, next) => Failure(msg, next)
  }
}

Parser { in =>
  Parser.this(in) match {
    case Success(r, n) => Parser { in => p(in).map(b => new ~(r, b)) }(n)
    case Failure(msg, next) => Failure(msg, next)
  }
}

Parser { in =>
  Parser.this(in) match {
    case Success(r, n) => p(n) match {
      case Success(r1, n1) => Success(new ~(r, r1), n1)
      case Failure(msg, next) => Failure(msg, next)
    }
    case Failure(msg, next) => Failure(msg, next)
  }
}
  

2014年11月18日火曜日

CentOS7にLAMP環境を構築してWordpressをインストールする

インターン生の阿達です。

いつかはこのブログや、会社のHPも自分で作れたらいいなあと思っている
プログラミング歴1か月のぺーぺーです。

その野望の第一歩として与えられた課題が
CentOS7にLAMP環境を構築してWordpressをインストールする
でした。
この記事では勉強した内容の復習を兼ねて手順を丁寧に紹介したいと思います。



目次

【1】LAMP( Apache + MariaDB(Mysql) + PHP )をインストールする

 0,準備
 1,Apacheをインストールする
 2,MariaDBをインストールする
 3.PHPをインストール

【2】Wordpressをインストールする
 
 1,Wordpressをダウンロードする
 2,MariaDBにWordpress用のデータベースを作成する
 3,Wordpressのセットアップをする
 4,




【1】LAMP( Apache + MariaDB(Mysql) + PHP )をインストールする

0,準備


○ユーザーをルートに切り替える

sudo su
su


○SELinux無効化

vi /etc/sysconfig/selinux


下記を変更
SELINUX=enforcing
↓
SELINUX=disabled

・OSを再起動します

shutdown -r now


○firewalld設定・現在の設定の確認

firewall-cmd --list-all-zones


(略)
public (default, active)
interfaces: enp0s3 enp0s8
sources:
services: dhcpv6-client ssh
(略)
初期設定では publicゾーンに ssh のみ許可されています。


・追加で http と https を許可設定します

firewall-cmd --add-service=http --zone=public --permanent
firewall-cmd --add-service=https --zone=public --permanent


・設定を読込みます

firewall-cmd --reload



・設定を確認します

firewall-cmd --list-services --zone=public


(下記表示であればOK)
dhcpv6-client http https ssh



1,Apacheをインストールする

yum install httpd

・.Apacheの設定を行う

vi /etc/httpd/conf/httpd.conf



・ここでは必要最小限のサーバー名(今回はexample.com)だけ設定しておきます

ServerName example.com:80



・Apacheのデーモンを起動する

・ウェブサーバー(httpd)を起動します

systemctl start httpd.service



・ウェブサーバー(httpd)のデーモン(サービス)が起動しているか確認します

systemctl list-units |grep httpd


(下記表示であればOK)



httpd.service        \
    loaded acitive running The Apache HTTP Server




・次に、ウェブサーバー(httpd)デーモンがブート時に自動起動するように設定しておきます。

systemctl enable httpd.service
以下のように表示されてればOK
ln -s '/usr/lib/systemd/system/httpd.service' '/etc/systemd/system/multi-user.target.wants/httpd.service'


・ウェブサーバー(httpd)デーモンの登録状態を確認します。
systemctl list-unit-files |grep httpd
(下記表示であればOK)
httpd.service         enabled


・ブラウザから先程サーバー名で設定したアドレス(この場合はexample.com)にアクセスすると、
Apacheの「Red Hat Enterprise Linux Test Page」が表示されます。







2,MariaDBをインストールする


yum install mariadb-server mariadb



・MariaDBのデーモンを起動する

・mariadbデーモンが起動しているか確認します。

systemctl list-units |grep mariadb



・何も出力されないので、起動していない状態だとわかります。

・次に、mariadbデーモンの登録状態を確認します。

systemctl list-unit-files |grep mariadb

mariadb.service        disabled

disabledなので、再起動してもmariadbデーモンは起動しません。


・mariadb のデーモン(サービス)を起動します。

systemctl start mariadb.service



・mariadb のデーモン(サービス)が起動しているか確認します。

systemctl list-units |grep mariadb
(下記表示であればOK)
mariadb.service                \
   loaded active running MariaDB database server



・次に、mariadbデーモンの登録状態を確認します。

systemctl list-unit-files |grep mariadb

mariadb.service        disabled
disabledなので、再起動してもmariadbのデーモンは起動しません。


・次に、mariadbデーモンがブート時に自動起動するように設定しておきます。

systemctl enable mariadb.service
(下記表示であればOK)
ln -s '/usr/lib/systemd/system/mariadb.service' '/etc/systemd/system/multi-user.target.wants/mariadb.service'


・再度、mariadbデーモンの登録状態を確認します。

systemctl list-unit-files |grep mariadb
(下記表示であればOK)
mariadb.service        enabled
enabledなので、再起動してもmariadbデーモンは起動されます。


・MariaDBのセキュリティ設定をします。

/usr/bin/mysql_secure_installation



OTE: RUNNING ALL PARTS OF THIS SCRIPT IS RECOMMENDED FOR ALL MariaDB
      SERVERS IN PRODUCTION USE!  PLEASE READ EACH STEP CAREFULLY!

In order to log into MariaDB to secure it, we'll need the current
password for the root user.  If you've just installed MariaDB, and
you haven't set the root password yet, the password will be blank,
so you should just press enter here.

Enter current password for root (enter for none): 【空enter】
OK, successfully used password, moving on...
Setting the root password ensures that nobody can log into the MariaDB
root user without the proper authorisation.
Set root password? [Y/n] 【Y】
New password: 【パスワードを設定する、今回はhoge】
Re-enter new password: 【先程のパスワードを入力する、今回はhoge】
Password updated successfully!
Reloading privilege tables..
 ... Success!
By default, a MariaDB installation has an anonymous user, allowing anyone
to log into MariaDB without having to have a user account created for
them.  This is intended only for testing, and to make the installation
go a bit smoother.  You should remove them before moving into a
production environment.
Remove anonymous users? [Y/n] 【Y】
 ... Success!
Normally, root should only be allowed to connect from 'localhost'.  This
ensures that someone cannot guess at the root password from the network.
Disallow root login remotely? [Y/n]【Y】
 ... Success!
By default, MariaDB comes with a database named 'test' that anyone can
access.  This is also intended only for testing, and should be removed
before moving into a production environment.
Remove test database and access to it? [Y/n]【Y】
 - Dropping test database...
 ... Success!
 - Removing privileges on test database...
 ... Success!
Reloading the privilege tables will ensure that all changes made so far
will take effect immediately.
Reload privilege tables now? [Y/n]【Y】
 ... Success!
Cleaning up...
All done!  If you've completed all of the above steps, your MariaDB
installation should now be secure.
Thanks for using MariaDB!


・ログインできるか試す。

mysql -h localhost -u root -p




【】で示した部分が必要な操作です。


Enter password:【先程設定したパスワードを入力する、今回はhoge】

Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 4
Server version: 5.5.37-MariaDB MariaDB Server
 
Copyright (c) 2000, 2014, Oracle, Monty Program Ab and others.
 
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
 
MariaDB [(none)]>【 exit】

Bye

3,PHPをインストールする

・PHPをインストールする

yum install php-mysql php php-gd php-mbstring



・phpがインストールできたか、バージョンをチェックしてみます。

php --version

PHP 5.4.16 (cli) (built: Jun 10 2014 02:52:47)
Copyright (c) 1997-2013 The PHP Group
Zend Engine v2.4.0, Copyright (c) 1998-2013 Zend Technologies

・httpdサービス(Apache)を再起動する

systemctl restart httpd.service



・php動作確認用のファイルを作成する。

cd /var/www/html
# echo '' > index.php


・上記で作成したindex.phpへブラウザからアクセスする。
(今回はexample.com/index.php)

phpの画面が表示されます。



【2】Wordpressをインストールする

1,Wordpressをダウンロードする

・最新のWordpressをダウンロードする

cd /tmp
wget http://wordpress.org/latest.tar.gz


・ダウンロードしたWordpressを解凍する

tar -xvzf latest.tar.gz -C /var/www/html


2,MariaDBにWordpress用のデータベースを作成する

・MariaDBにrootでログインする

mysql -h localhost -u root -p


・データベースを作成する

今回testとhogehogeで示した部分はご自分で設定してください
CREATE USER test@localhost IDENTIFIED BY "hogehoge";
CREATE DATABASE test_blog;
GRANT ALL ON test_blog.* TO test@localhost;
FLUSH PRIVILEGES;
exit


3,Wordpressのセットアップをする


・localhostとWordpressを紐づける

/etc/hosts に下記を書き足す

127.0.0.1 wordpress


・wp-config.phpを生成する

cd /var/www/html/wordpress

cp wp-config-sample.php wp-config.php


・wp-config.phpでWordpressの設定をする

vi wp-config.php

【2】2,で作成したデータベースの情報を入力します
define('DB_NAME', 'test_blog');
define('DB_USER', 'test');
define('DB_PASSWORD', 'hogehoge');
define('DB_HOST', 'localhost');
define('DB_CHARSET', 'utf8');
define('DB_COLLATE', '');
(中略)
$table_prefix = 'wp_';
define ('WPLANG', '');
define('WP_DEBUG', false);

・example.com/wordpressにアクセスして設定をする。
指示に従って設定していけばOKです。
以上になります。
私がインストールしたときには、SELinuxで詰まったりしていたので
基礎の基礎から書いてみました。
備忘録兼ねてですが、どなたかの参考になれば幸いです。