esm アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

[復刻]ドキュメント記述の心構え

2004年中途入社の koic です。来月で入社17年。長いですね。 17年の間で人や技術が流転する長い勤務の中で伝承の途切れを観測していることから、私がいまの勤務先に入ったころ Wiki への考え方への影響を受けたものを発掘してみました。今回、私的解釈を交えて一般公開します。

はじめに

本記事で取りあげるのは 2006年以前に「ドキュメント記述の心構え」 (WikiName は DeciplinesOfWritingDocumentsOnWiki) として作成された Wiki ページを掘り起こしたものです。かつて角谷さんが牽引していたプロジェクトにて引き継がれていた文書で原文ママで置いています。Wiki 記法は数多ある Wiki クローンの Ruby 製の Hiki による記法です。

『ドキュメント記述の心構え』原文

{{toc}}

! 木構造よりも、ネットワーク構造を好め
* Wikiページはツリーノードというよりは、ネットワークノードだと思え。
* 孤立したノード間を繋げるものがハイパーリンクである。
* ただし、視点ごとに整理した木構造のインデックスページの作成は、奨励される。

! ネストしたリストよりも、見出しによる構造化を好め
*リストは使い易いが、それゆえに濫用しがち。それは本当にリストか?
* リストはインデントが深い
* 見出しを使えばTOCプラグインでページの目次を自動生成できる

! リストで文章を区切るよりも、空行をあけてPタグでレンダリングせよ
* それは本当にリストか? 簡潔な地の文の連続を、適切なまとまりで段落とせよ

! 安易に項目追加する前に、リオーガナイズを検討せよ
* 特定カテゴリの一覧となるインデックスページを適切なサイズで分割せよ

! 正規化と重複の均衡点を探せ
* DRYと読み下しやすさとのバランスに唯一無二の解決策はない。コンテキスト毎での最適なバランスを探せ

! PREとQUOTEを使い分けよ
* 難しいよね

! リオーガナイズのメカニクスを用意せよ
* TODO: まとめてみること :-)

私的解釈

掘り起こした文書への私的解釈です。

{{toc}}

見出しによる目次 (Table of Contents) の生成を作ることをページ自体にも適用して、ドキュメント自体がサンプルになっている好例。

木構造よりも、ネットワーク構造を好め

のちの江渡さんの『パターン、Wiki、XP ~時を超えた創造の原則』に通じるので、そちらを読むと理解が深まると思います。

ネストしたリストよりも、見出しによる構造化を好め

これは本当にそう。自分も個人ブログの方にエントリにしている。ここではさらに見出しにすべき理由として TOC の生成を挙げている点が秀逸。

koic.hatenablog.com

リストで文章を区切るよりも、空行をあけてPタグでレンダリングせよ

その辺の書籍などを読んでも箇条書き中心で構成されているものはレアだと思う。散文的な箇条書きに頼るだけではない文章を書く力は大事。

安易に項目追加する前に、リオーガナイズを検討せよ

これはちょっと難しかった。問題の背景をうまく掴めていないので (あるいは忘れてしまったようで) 、よく理解できていない。

正規化と重複の均衡点を探せ

同一の文章がページにまたがる物差しについて挙げられていると思う。コンテキストによって補完される内容であれば重複して問題ないと思う。一方でコンテキスト非依存であれば独立したページとして参照されるものだと思っている。DCI (Data Context Interaction) はヒントになりそう。

PREとQUOTEを使い分けよ

コードは PRE を使い、引用は QUOTE を使うで良いと思うが、当時の疑問を分かっていない (私は角谷さん卒業後の後期に参加していた) 。

リオーガナイズのメカニクスを用意せよ

ここで未完となっていて、続きは各人で Wiki を育てていこうといった感じになっていると受け取っている。この記事を種子にして育つと嬉しい。

、、、とここまで書いたあとフェローの角谷さんからコメントをいただく機会がありました。いわくこの項目は「qwik から繋がり 2020年のいま Scrapbox で実現されている」とのことでした。Scrapbox を使った事例の URL をあげておきます。

scrapbox.io

続きは各自の研究に譲ります。

おわりに

現代ではネットワークを辿る方法に限らず全文検索するという手法がより一般的になっていますが、ハイパーテキストハイパーリンクからなる Web ページの文書構造や相関関係というインターネットの基礎概念のひとつについて考える良いきっかけになればと思い発掘してみました。その考え方は Wiki ページに限らず Markdown や AsciiDoc による文書作成にも転用できるでしょう。

以上、3年越し17年ものの復刻作業結果の共有です。個人的にはのちのドキュメントの書き方にも影響を受けており、かつてプレゼンテーションとして起こしたりしました。文書構造に対して新たに考えるきっかけになれば幸いです。

We are going to the Timeless Way of...

f:id:koic:20201014165657p:plain


創業40年の永和システムマネジメントでは一緒にソフトウェア開発のアカシックレコードを刻んで行こうというメンバーを募集しています。

agile.esm.co.jp

プロジェクトを『アジャイルサムライ』からふりかえる

こんにちは、最近自作キーボードに夢中な @kasumi8pon です。

最近 Rails アプリケーションを 2つ、 Rails 4 系 からバージョン 6.0 にアップデートするプロジェクトをやっていました。 1月に入社してから9ヶ月が経ち、初めてわたしがメインとなって進めたプロジェクトでした。 いろんな方のご協力のおかげで無事に Rails 6.0 のアップデートがリリースできました 🎉

話はかわりますが、現在アジャイル事業部では週に一度のペースで アジャイルサムライ の読書会が開催されていて、わたしも参加しています。 読書会では現実のプロジェクトで起こっている実際の話を聞けて、とても面白い会になっています。

今回わたしが担当したプロジェクトではアップデート作業のみという特殊な状況であったため、ユーザーストーリーを出して、見積もりをして、イテレーションの計画を立てて…といういわゆるアジャイル開発をしようとは思ってはいませんでした。 それでも、ふりかえってみると アジャイルサムライ で学んだ視点から見てよかったと思う点があったので、それらをあげてみようと思います。

スコープを調整することができた

アジャイルサムライの第5章に、「何を諦めるかはっきりさせる」という節があります。 プロジェクトにおよぶ様々なフォース(影響)のうち、すべてを最優先にしようとすると、プロジェクトはうまく行かなくなってしまうというお話です。 ここでは、特にプロジェクトと固く結びついたフォースとして、時間、予算、品質、スコープが紹介されています。

このプロジェクトでは、時間、予算、品質は固定されており、スコープを調整しました。 基本的にはゴールは Rails のバージョンを上げて、テストを通し、チェックリスト通りアプリケーションが動作することでした。 しかし、アップデートをする際には、バージョンを上げるために影響範囲が大きい gem や、Deprecated であり、本来は他の機能に置き換えるべき gem が存在しました。 すべてに対応することは不可能であったため、どこまで対応するか相談した結果、今回のプロジェクトではデータ移行等の作業が不要な範囲でできる限り、 gem のバージョンを上げることにしました。 最終的には、Rails 自体のバージョンアップ作業は完了し、バージョンが古いままだった多くの gem も最新までアップデートすることができました。

関係者との意思疎通がうまくできた

10章のアジャイルな意思疎通の作戦では、イテレーションで開催すべき4つのミーティングと、1日の始まりに行うデイリースタンドアップが紹介されています。

このプロジェクトでは、「前週でやったこと」、「次週でやること」、「課題」の3点を週次で共有していました。 これはイテレーション計画ミーティングやミニふりかえりに少し似ているかもしれません。

一方、デイリースタンドアップは行いませんでした。なぜなら、問題があったときにすぐ相談できる環境にあったからです。マスターセンセイも、『いつでも気軽にチームメンバー同士やお客さんとの間で相談できるなら、別に毎朝スタンドアップミーティングをやらずともうまくいくかもしれん』と言っていましたね😉

おわりに

こうしてみると、"アジャイル開発"をしよう!と思っていなくても、アジャイル開発のよいところを取り入れることでうまくいくことがあるかもしれないです。 "アジャイル開発"自体を目的にするのではなく、チームにあった方法を模索しながら、これからもやっていきたいと思います。

Rails 6.1 で導入される予定の delegated_type をつかってみてる話

こんにちわ。はじめまして。@kajisha です。 推しの Vtuber は、因幡はねる組長 かわいいおそろしいあくまでびでび・でびる様です。

最近は、風見くくさんのものまねが好きすぎてよく観てます。 初 3D 配信はほんとにクオリティが高いのでぜひ観てみてください。

www.youtube.com

このエントリでは、現プロジェクトでつかっている delegated_type について少し書いてみたいと思います。

delegated_type とは何か

以下の Pull Request で Active Record に導入されたものです。

github.com

Active Record にはクラス継承のしくみとして STI が提供されていますが、 単一のテーブルにサブクラスに必要な属性ももたせることになるので それぞれのサブクラスに必要な(そしてそれらはほとんどが nullable な)カラムを持つ巨大でスカスカなテーブルを作らざるを得ません。 もちろん、サブクラスの属性がほぼ同じ場合は STI でもあまり問題はないと思います。

しかしながら、現実はそうもいかないので、サブクラスごとに違う属性を持たせたくなることはままあります。 delegated_type はこのケースに対しスーパクラスに対応するテーブルとサブクラスに対応するテーブルのレコードの2つで継承関係を表現します。

以降で delegated_type を使って弊社組織の構造を実装してみるサンプルをもとにどんな感じなのか書いていきたいと思います。

弊社の組織構造を delegated_type をつかって表現してみる

それでは実際に弊社の組織構造をモデリングしてみます。今回は、組織構造を柔軟に扱うことのできる責任関係パターンで表わしてみます。 責任関係パターンの詳細は、Martin Fowler のアナリシスパターン―再利用可能なオブジェクトモデル (Object Technology Series) を読んでいただいた方がわかりやすいと思います。 (アナパタ、なんで絶版になっちゃたんですかねぇ…)

今回は、責任関係パターンで会社 -- 事業部 -- 社員 という構造をつくってみたいと思います。

f:id:kajisha:20201002093719j:plain
責任関係パターン

このパターンはあくまでも概念モデルなので、直接実装するのはややむずかしいです。 うまく説明できるかちょっとあやしいのですが、知識レベルの型は、クラスに相当する(クラスのクラス。メタクラスですね)ものであり、知識レベルのレイヤーでパーティ間の制約を表現します。 操作レベルは、知識レベルの制約を担保したオブジェクト群、と考えるとわりと近いのかもしれません。

しかし、このまま実装するのはルナティックモード*1すぎるので、ちょっと簡略化して以下のようにしてみました。

f:id:kajisha:20201002093758j:plain
実装

責任関係型は、責任関係に関連のタイプを列挙型として表現することにしました。パーティ型は、それぞれパーティのサブクラスとしています。 なお、これが責任関係パターンの正しい実装なのかというより、今回のサンプル用に変形したものである点は留意ください。 それでは、ひとつづつモデルをつくっていきます。

まず Rails アプリケーションを準備します。

bundle init

delegated_type はまだリリースされていないので master の Rails を指定します。

gem "rails", github: "rails/rails"
bundle exec rails new -d sqlite3 .

Party(パーティ) モデルの作成

bin/rails g model Party partyable:references{polymorphic}

ここで、polymorphic アソシエーションとして partyable を作成していますが直接 delegated_type なアソシエーションを一発で作成できないので一旦、ポリモーフィック関連として作成します。 なぜ polymorphic アソシエーションとして作成するのかと言うと、delegated_type は内部でサブクラスの情報を polymorphic アソシエーションとして扱っているからです。 ここはのちほど、delegated_type で書き直します。

すると以下のようなモデルが生成されます。

class Party < ApplicationRecord
  belongs_to :partyable, polymorphic: true
end

Compnay/Department/Employee(パーティのサブクラス) モデルの作成

次に Party のサブクラスをそれぞれ作成します。

Company モデル

bin/rails g model Company name:string
class Company < ApplicationRecord
end

Department モデル

bin/rails g model Department name:string
class Department < ApplicationRecord
end

Employee モデル

bin/rails g model Employee name:string email:string
class Employee < ApplicationRecord
end

delegated_type を Party モデルに指定する

class Party < ApplicationRecord
  belongs_to :partyable, polymorphic: true
end

を次のように書き換えます。

class Party < ApplicationRecord
  delegated_type :partyable, types: %w(Company Department Employee)
end

そして Company/Department/Employee が Party として振舞うことを示すために、Partyable concern を作成してサブクラス側に include させます。

module Partyable
  extend ActiveSupport::Concern

  included do
    has_one :party, as: :partyable, touch: true
  end
end

この concern を Company/Department/Employee に include させます。

class Company < ApplicationRecord
  include Partyable
end

class Department < ApplicationRecord
  include Partyable
end

class Employee < ApplicationRecord
  include Partyable
end

Accountability(責任関係) モデルの作成

責任関係モデルを作成します。責任関係型の列挙型はのちほど追加していきます。

bin/rails g model Accountability accountability_type:integer started_at:datetime ended_at:datetime commissioner:references responsible:references

このままでは commissioner, responsible に対応するテーブルが解決できないので、マイグレーションを以下のように修正します。 :commissioner:responsibleforeign_keyto_table オプションがミソです。これで parties テーブルを参照できるようになります。 これを付けないと、それぞれ commissioners, responsibles テーブルがあると判断されてしまうので、parties テーブルとのアソシエーションであることをおしえてあげます。

class CreateAccountabilities < ActiveRecord::Migration[6.1]
  def change
      create_table :accountabilities do |t|
      t.integer :accountability_type, null: false
      t.datetime :started_at, null: false
      t.datetime :ended_at, null: false, default: '9999-12-31 23:59:59.999Z'
      t.references :commissioner, null: false, foreign_key: {to_table: :parties}
      t.references :responsible, null: false, foreign_key: {to_table: :parties}

      t.timestamps
  end
end

Accountability にパーティ間の有効期間を表す started_atended_at を追加していますが、今回のサンプルでは本質ではないので使ってません。

つぎに、Party モデルと Accountability モデルのアソシエーションを追加します。

class Accountability < ApplicationRecord
  attribute :started_at, default: -> { Time.current }

  belongs_to :commissioner, class_name: 'Party', inverse_of: :commissioners
  belongs_to :responsible, class_name: 'Party', inverse_of: :responsibles
end
class Party < ApplicationRecord
  delegated_type :partyable, types: %w(Company Department Employee)

  has_many :commissioners, class_name: 'Accountability', inverse_of: :commissioner, foreign_key: :responsible_id, dependent: :destroy
  has_many :responsibles, class_name: 'Accountability', inverse_of: :responsible, foreign_key: :commissioner_id, dependent: :destroy
end

delegated_type により、いくつかの scope や predicate メソッドが生成されますが、ここでは説明を省略します。

実際にオブジェクトを作って確かめてみる

さて、準備はできました。それでは実際にパーティ間のアソシエーションをつくってみましょう。 なお、以下のシナリオでおもむろに Accountability に accountability_type の値を追加していますが、 実際は責任関係型がみたすべき制約をバリデーションとして書くべきです。

シナリオ1 会社と事業部の関係をつくる

永和システムマネジメントにアジャイル事業部がある、という関係をつくってみます。 さきほど責任関係のタイプは、列挙型で表現することにしたので、会社と事業部の関連をあらわす enum を Accountability に追加します。 企業と事業部の責任関係型を organization_structure としてみます。

class Accountability < ApplicationRecord
  attribute :started_at, default: -> { Time.current }

  belongs_to :commissioner, class_name: 'Party', inverse_of: :commissioners
  belongs_to :responsible, class_name: 'Party', inverse_of: :responsibles

  enum accountability_type: {organization_structure: 0}
end

では永和システムマネジメントにアジャイル事業部をぶらさげてみましょう。

company = Party.create!(partyable: Company.create!(name: '永和システムマネジメント'))
department = Party.create!(partyable: Department.create!(name: 'アジャイル事業部'))
Accountability.organization_structure.create!(commissioner: department, responsible: company)

シナリオ2 事業部と社員の関係をつくる

OK. それでは、アジャイル事業部に社員をひとり追加してみましょう。この責任関係型は、belong としてみましょう。ちょっと名前がイマイチですね…ゆるしてください。

employee = Party.create!(partyable: Employee.create!(name: 'wat-aro', email: 'wat-aro@example.com'))
Accountability.belong.create!(commissioner: employee, responsible: department)

よさそうじゃないですか。

関連をたどっていろいろデータが欲しいんだ!

ここからはいくつかのテーブルを経由して取得していくことになるので、ちょっと一筋縄でいきません。いくつか準備が必要です。 必要な道具は has_many through:, source: です。

企業にある事業部の一覧を取得したい

Company にある Department は、[:Company] <-- [:Party] <-responsible- [organization_structure:Accountability] -commissioner-> [:Party] --> [:Department] をたどって取得します。 最終的には

class Company < ApplicationRecord
  ...
  has_many :departments, ...
end

のようなアソシエーションで取得できることを目指しますが、 一気呵成にアソシエーションをたどれないので、ひとつづつ解決していきます。なぜたどれないのかは、最後の例で説明します。

organization_structure な Accountability を取得する

次のアソシエーションを取得します。 [:Company] <-- [:Party] <-responsible- [organization_structure:Accountability]

class Company < ApplicationRecord
  include Partyable

  has_many :organization_structure_accoutabilities, -> { organization_structure }, through: :commissioners, source: :party
end

organization_structure_accoutabilities から Party を取得する

上記で作成したアソシエーションからさらに以下のアソシエーションを取得します。 [organization_structure:Accountability] -commissioner-> [:Party]

class Company < ApplicationRecord
  include Partyable

  has_many :organization_structure_accoutabilities, -> { organization_structure }, through: :commissioners, source: :party
  has_many :organization_structures, through: :organization_structure_accoutabilities, source: :commissioner
end

Department を取得する

さいごに :organization_structures を経由して Department を取得します。

class Company < ApplicationRecord
  include Partyable

  has_many :organization_structure_accoutabilities, -> { organization_structure }, through: :commissioners, source: :party
  has_many :organization_structures, through: :organization_structure_accoutabilities, source: :commissioner
  has_many :departments, through: :organization_structures, source: :partyable, source_type: 'Department'
end

事業部に所属する社員の一覧を取得したい

Department に所属している Employee は、[:Department] --> [:Party] <-responsible- [belong:Accountability] -commissioner-> [:Party] --> [:Employee] をたどります。

Company の例と同様に、Accountability, Party, Employee と解決していきます。

class Department < ApplicationRecord
  include Partyable

  has_many :belong_accountabilities, -> { belong }, through: :party, source: :commissioners
  has_many :belongs, through: :belong_accountabilities, source: :commissioner
  has_many :employees, through: :belongs, source: :partyable, source_type: 'Employee'
end

企業に所属する社員の一覧を取得したい

ここまでで、企業の事業部、事業部に所属する社員を取得できるようになりました。これらのアソシエーションをつかって企業に所属する社員、というのも取れそうです。

これは Company モデルに以下のような employees アソシエーションを追加することで取得できます。

class Company < ApplicationRecord
  ...
  has_many :departments, through: :organization_structures, source: :partyable, source_type: 'Department'
  has_many :employees, through: :departments, source: :employees, dependent: :destroy
end

なぜアソシエーションを段階的に辿ったのか?

Company のインスタンスから employees を取得するときの Active Record が実行する SQL に理由があります。

SELECT "employees".*
  FROM "employees" INNER JOIN "parties" ON "employees"."id" = "parties"."partyable_id"
                   INNER JOIN "accountabilities" ON "parties"."id" = "accountabilities"."commissioner_id"
                   INNER JOIN "parties" "parties_employees" ON "accountabilities"."responsible_id" = "parties_employees"."id"
                   INNER JOIN "departments" ON "parties_employees"."partyable_id" = "departments"."id"
                   INNER JOIN "parties" "organization_structures_employees" ON "departments"."id" = "organization_structures_employees"."partyable_id"
                   INNER JOIN "accountabilities" "organization_structure_accountabilities_employees" ON "organization_structures_employees"."id" = "organization_structure_accountabilities_employees"."commissioner_id"
                   INNER JOIN "parties" "parties_employees_2" ON "organization_structure_accountabilities_employees"."responsible_id" = "parties_employees_2"."id"
  WHERE "parties_employees_2"."partyable_id" = 1
    AND "parties_employees_2"."partyable_type" = 'Company'
    AND "parties_employees"."partyable_type" = 'Department'
    AND "organization_structure_accountabilities_employees"."accountability_type" = 0
    AND "organization_structures_employees"."partyable_type" = 'Department'
    AND "accountabilities"."accountability_type" = 1
    AND "parties"."partyable_type" = 'Employee'

has_many through するときの INNER JOIN に同じテーブルがある場合、別名を付けるためです。もし、これを where で書いていた場合は、どのテーブルに対しクエリを書いているのかが、わからないため正しくレコードを取得できないことになります。

ここの部分は、わたしもこれでいいのかまだ自信がないので、もっとよい方法があればぜひ教えてください。

さいごに

いかがでしたでしょうか?わたしもまだ手探り状態なのでもっと良い方法があるかもしれません。そのときはまたなにか書ければいいなと思います。

以下にこのエントリで作成したサンプルを置いておきますので、ご興味あるかたはご覧いただければと思います。

github.com

*1:東方Projectのゲームにおける最高難易度モード

Rails / OSS パッチ会オンライン 2020年10月のお知らせ

2020年10月の Rails / OSS パッチ会を 10月8日(木)にオンライン開催します。

この会をひとことでいうと、日頃のお仕事で使っている Rails をはじめとする OSS への upstream にパッチを送る会です。

会には RubyRails のコミッターである顧問の a_matsuda もいますので、例えば Rails に送るパッチのネタがあるけれど、パッチを送るに適しているかの判断やパッチを送る流れが悩ましいときなど a_matsuda に相談して足がかりにするなどできます。

開催時間は 17:00-19:00 となりますがご都合のあう方はぜひご参加下さい。Google Meet あたりのテレビ会議システムを使います。

当日の招待 URL は Idobata の esminc/rails ルームで共有する予定です。

idobata.io

特に募集ページなど設けませんが、上記理由から Idobata のアカウントと (TV 会議システムによっては Google アカウント) が必要になると思います。

以下、前回の活動が関わる成果です。

koic: RuboCop

github.com

yahonda: activerecord-oracle_enhanced-adapter

github.com

その他の開催方針については以下の Gist に記していますので、ご参照ください。

gist.github.com

XP祭り2020に参加して野良LTしてきました

こんにちは。@junk0612 です。

シルバーウィークの初日9/19(土)に、XP祭り 2020 が開催されました。 それに参加してきましたので、感想などを書いていこうと思います。

野良LT

XP祭りには 野良LT という企画があります。 お昼の時間を利用して、飛び入りでLTしたり、午後のタイムテーブルの内容を眺めたり、勉強会の告知をしたりします。 もちろんお昼なので参加は任意だし、途中参加・途中抜けOKというゆるいけれどXP祭りらしい楽しい時間です。

実は、僕はもともとタイムテーブルの終わりの方にある LT の時間に参加登録をしていたのですが、登録者多数で抽選となり、残念ながら落選してしまっていました。 野良 LT の存在をすっかり忘れていて「発表したい内容だったけどしょうがないな、まあ気楽に楽しもう」なんて思いながらのんきに朝から参加したのですが、 開会の「まんざい」で野良 LT の話が出てハッとなり、午前の基調講演を聞きながら急いで間にあわせの資料を作って発表してきました。 発表資料はこちらです。

内容を一言で表すと「アジャイルって考え方とか取り組む姿勢が一番大事だよね」という感じです。 アジャイル開発は、世間にはよく「開発手法」という側面で紹介されることが多いですが、『達人プログラマー』 (「常に心に聖堂を思い描く」) や、『アジャイルサムライ』 (「信頼貯金を増やす」) 、『エクストリーム・プログラミング』 (「XPとはソーシャルチェンジだ」) などに書かれている通り、今直面している問題だけでなく、最終的な目標や周囲の状況を正しく捉え、そのなかでよりよい解決方法を探していく心構えこそがアジャイルの本質なんだろうな、ということを最近よく考えていたので、それを素直にそのまま書いています。

書いている途中で「そもそも『アジャイルソフトウェア開発宣言』だって価値の話をしていたじゃないか。もしかしてすごく今更なことを言っているのでは……?」という疑念が浮かびましたが、それはそれ。 今後も変わっていくであろう自分の中の考えをスナップショットとして残しておくことを優先しました。 準備もそれほどしていない中で拙い発表でしたが、温かい反応をいただけてよかったです。「ぜひこれを読んでほしいなー」というリンクもコメントでいただいて、発表したかいがあったなと思いました。

面白かった発表

意外に野良 LT の話が長くなってしまったのですが、参加した中で面白かった発表を手短に2つほどご紹介しようと思います。

エンジニアの創造力を解き放て! XPと歩んだ17年・技術・現場・仲間

基調講演です。大企業の中で17年もの間アジャイル開発の普及に挑んできた発表者の神部さんの経験や工夫の詰まった興味深い発表でした。 大企業ならではのとんでもない状況や事態がたくさんあり、信じられなかったと同時に、聴衆のみなさんが笑っているのを見てきっと似たような経験があるのだろうと思いました。 アジャイル開発が「ふつう」になるにはまだまだ頑張らないといけないんだなあ、と (LT の資料作成で慌てつつ) 身の引き締まる思いで聞いていました。 発表資料はこちらに公開されています。

近代史とアジャイル

われらが @fkino の発表です。 「近代 (資料内では1776年からにスポットライトを当てています) における産業の歴史の中で、アジャイルソフトウェア開発はどのような立ち位置にいるのか?」という、面白い視点での発表でした。 アメリカ独立時に制定された合衆国憲法の中にすでにアジャイルマニフェストの精神が見出されるところや、近代合理主義と脱近代合理主義の思想に触れ、ドラッカーの言葉を引用しながら「それには現代ではアジャイルという名前がついている」というところなど、詳しくない分野が多いながら聞いていて面白かったです。 発表資料はこちらに公開されています。

全体を通して

COVID-19 の影響で、様々なカンファレンスがそれぞれの形で実施されていますが、XP祭りはオンライン開催となって自宅からゆったり参加できてよかったです。 また以前のように対面で恐れることなく会話ができるようになることを祈りつつ、これからも頑張っていくぞ、と気持ちを新たにしたイベントでした。

たくさんのファイルを巨大なアーカイブファイルにしてアップロードする

たくさんのファイルを同時に扱う際には、その処理が使うリソースに注意したいものです。さいきん私が直面した課題は、AWS S3 のあるバケットに保存されている大量のファイルをひとつのアーカイブファイルにして別のバケットに配置するというものでした。ひとつのアーカイブに含まれるファイルの数は最大で70万くらいあり、各ファイルのサイズは 10MiB を上限にさまざまです。ちなみに、アーカイブ対象のファイルをすべて合わせると、140GiB 以下であることがわかっています。

もしも70万のファイルを一気にダウンロードしてZIPアーカイブしようとしたら、ある程度のディスク容量が確保できていることを事前に確認しておいたほうがよさそうです。どれくらい必要でしょうか。もしアーカイブ処理を同じマシン上で平行して実行すると、必要な容量はさらに掛け算で増えていきそうです。そういえば、できあがったアーカイブファイルは数十GiBのサイズになる可能性がありそうですが、そもそも、果たしてそんなに大きなファイルは一度にアップロードできるんでしょうか。心配事がいろいろあります。こういった場面では、一度にすべて片付ける代わりに、少しずつ処理していくパイプラインっぽいやり方で対応できるとよさそうです。

AWS S3 Multipart Upload で少しずつアップロードする

幸い、S3 は巨大なファイルを小さなパーツに分割して少しずつアップロードする "multipart upload" と呼ばれる手段を用意しています。"multiple upload" は、アップロード対象のファイルが完成していなくても開始できるようになっています。つまり、どこかにアップロードするために、アーカイブファイルをいったん手元のファイルシステムに保存する必要は必ずしもないということです。

RubyAWS SDK も、もちろんこの機能を使う API を備えています。以下はその一例です。

s3_client = Aws::S3::Client.new(region: 'ap-northeast-1')
archive_object = Aws::S3::Object.new(bucket, 'archive.zip', client: s3_client)
archive_object.upload_stream do |upload_stream|
  upload_stream.binmode

  # write to upload_stream (one side of a pipe, IO object for writing)
end

Aws::S3::Object#upload_stream で miltiple upload の開始を知らせると、AWS SDK はブロック引数を介して IO オブジェクトを返してきます。これは IO.pipe で作成されたパイプの書き込み側なのですが、とにかく、ここにどんどんファイルの中身を書き込んでいけばライブラリが勝手にファイルを適当なサイズ (デフォルトは 5MiB) で分割しつつ、少しずつS3バケットにアップロードしてくれます。べんり!

ディスクに書き込まずに ZIP ファイルを作る

ファイルをアーカイブするとき、できあがったアーカイブファイルをまるごと保存するためのディスク容量を用意する必要がないといいですね。実は、ZIPはそういう要求にうってつけのフォーマットになっています。

ZIP フォーマットの構造

ほかのアーカイブフォーマットと同じようにZIPファイルも、アーカイブしているファイルの名前やファイルサイズ、データ位置といったメタデータを保持している場所があります。"central directory header" と呼ばれる領域で、興味深いことに、"header" という名前に反して ファイルの末尾に配置されることになっています

つまり、ZIPファイルを作成し始める段階では、すべてのアーカイブ対象のファイルの情報を手元に持っていなくても問題ありません。それらの情報は最後に central directory を書き込むときに用意できていれば十分です。それまでは、ファイル名などのメタデータをとっておきつつ、単純に圧縮したファイルをひとつずつ頭から書き込んでいけば大丈夫です。ファイルの中身を圧縮してアーカイブに追加できた時点で、元のファイルは不要になります。ですので、ZIP形式のアーカイブを作る際に、対象のすべてのファイルをあらかじめ手元のファイルシステムに保存しておく必要はありません。

パイプと zip_tricks gem

今回は ZIP アーカイブを作成するために zip_tricks gem を選択しました。理由は、このライブラリではZIPファイルの出力先としてパイプを使えるためです。ZipTricks::Streamer.open はZIPファイルの出力先としてIOオブジェクトを受け取るのですが、このライブラリは受け取ったIOオブジェクトを rewind しないため、パイプを渡すことができます。

for_write, for_read = IO.pipe # for_read can be used to upload

ZipTricks::Streamer.open for_write do |zip|
  zip.write_deflated_file 'a-file.txt' do |input_stream|
    input_stream.write 'the content of a-file.txt'
  end
end

出力先にパイプが使えるということは、できあがったZIPアーカイブファイルを手元のファイルシステムに保存しないという選択肢がとれるようになります。そういえば、S3 の multipart upload のセクションで、Aws::S3::Object#upload_stream はブロック引数としてパイプのIOオブジェクトを提供していましたね。つまり zip_tricks を使うと、次のように、アーカイブの出力をそのパイプに直接書き込めるようになります。

archive_object.upload_stream do |upload_stream|
  upload_stream.binmode

  # upload_stream is a pipe
  ZipTricks::Streamer.open upload_stream do |zip|
    zip.write_deflated_file 'a-file.txt' do |input_stream|
      input_stream.write 'the content of a-file.txt'
    end
  end
end

サンプルコード全体

これで、手元でファイルを作成せずに、たくさんのファイルをアーカイブし巨大なアーカイブをアップロードできるようになりました。

require 'aws-sdk-s3'
require 'zip_tricks'

bucket = 'hibariya-sandbox'
s3_client = Aws::S3::Client.new(region: 'ap-northeast-1')

files_to_archive = %w[alpha bravo charlie delta] # whatever
archive_object = Aws::S3::Object.new(bucket, 'archive.zip', client: s3_client)

archive_object.upload_stream tempfile: false, part_size: 20 * 1024 * 1024, thread_count: 3 do |upload_stream|
  upload_stream.binmode

  ZipTricks::Streamer.open upload_stream do |zip|
    files_to_archive.each do |file_path|
      zip.write_deflated_file file_path do |input_stream|
        s3_client.get_object bucket: bucket, key: file_path, response_target: input_stream
      end
    end
  end
end

Aws::S3::Object#upload_stream に渡しているキーワード引数はすべて任意です。詳しくは 公式のドキュメント を確認してください。 ※話を簡単にするために、アップロード/ダウンロード両方に同じバケットを使用しています。

注意点など

AWS S3 の multiple upload を使う場合は、分割できるパーツの数や未完了のアップロードへの課金など、いくつか知っておいたほうが良い事項があります。おそらく、未完了のアップロードを定期的に掃除するためにS3バケットにライフサイクルポリシーを追加したくなるのではないかと思います。詳細は公式の記事をご確認ください。

https://docs.aws.amazon.com/AmazonS3/latest/dev/mpuoverview.html

Windowsのススメ WSL2を利用した快適な開発環境作り

こんにちは!コロナ禍の影響でライブハウス遊びができずに、気になるバンドのライブ配信をひたすら見続ける生活を送っている@hidenbaです。

アジャイル事業部では、各自が利用するPCは、予算に応じて2-3年のサイクルで買い換えを行い、各々が好きなPCを購入して利用しています。 以前はmacOSの利用率が圧倒的に高かったのですが、Dockerを利用して開発を行う際に、遅さがストレスになるという理由でLinuxを選択する 事が多くなってきているように思えます。かくいう私は、3年前まではMacを利用していましたが、同様の理由でLinuxを選択して、現在はThinkpad T490にArch Linuxを入れて利用しています。

私たちは、受託開発やお客様の業務支援を行っていますが、時として、お客様のセキュリティポリシーの都合上、お客様から貸与されたPCを利用して 開発作業を行うことがあります。そのときに支給されるPCはお客様が指定したPCになることが多く、OSの選択肢としてはmacOSがほとんどだったのですが、最近ではWindowsも選択肢に入るようになってきています。

WindowsRubyを使ったシステムの開発なんてできるのか?なんて思ったりもしたのですが、WSL2を利用することで快適に開発を行えるようになってきています。今日は、私がお客様から、貸与されたWindowsPCで開発できる 環境になるまでに、やったことをざっとあげていきます。それぞれの詳細については、公式サイトのドキュメントや詳しく解説されている記事が多数あるので割愛します。

ターミナルで作業するための準備

今回、お客様から貸与されたPCはDellXPS 13でした。初回起動後は、Windowsアップデートをして最新の状態にします。 次にコマンドラインから利用できるパッケージ管理ツールの「winget」をインストールします。 これで、コマンドラインを中心に作業ができるようなりました。

標準のPowerShellを使うのでもいいのですが、もうちょっと高機能で使いやすいWindows Terminalをインストールして以降は Windows TerminalPowerShellを利用して作業していきます

$ winget install "windows terminal"

必要なアプリケーションのインストール

必要なアプリケーションをwingetを利用してインストールしていきます。

内外とのコミュニケーションのパスを用意するためにSlackとTwitterのクライアントをインストールします。

$ winget install slack
$ winget install tweeten

エディタがないと生きていけないのでVSCodeをインストールします。

$ winget install vscode

WSL2の設定

WSL2を有効にするために、管理者権限でPowerShellを起動します。

$ Start-Process powershell.exe -Verb runas

仮想マシンプラットフォームオプションを有効にして再起動をかけます。

$ dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

※私の環境では、上記コマンドではWSLが有効化されなかったのでコントロールパネルから設定をしました

[コントロール パネル] -> [プログラムと機能] -> [Windows の機能の有効化または無効化] を開いて、 [LinuxWindows サブシステム] をチェックします。

WSLのバージョンを2に固定します。

$  wsl --set-default-version 2

カーネルコンポーネントの更新をしてWSL2が実行可能な状態にします。 https://docs.microsoft.com/ja-jp/windows/wsl/wsl2-kernel

WSL2にLinuxをインストールします。

$ winget install ubuntu

Docker環境の構築

Windows側にDockerをインストールします。

$ winget install "Docker Desktop"

VSCodeからWSL2環境に接続して開発を行うためにRemote Development拡張をインストールを行い、WSL2のubuntuからVSCodeを起動します。

$ code

開発に必要なリポジトリをWSL2のファイルシステムにcloneしてきてdocker-compose up等Docker環境を起動して開発を行うことができるようになりました!!

より快適な環境にしてくれるPowerToysのインストール

この状態だと、開発はできるがまだまだストレスが多い環境なので、キーバインドやウィンドウの配置を固定するためにPowerToysをインストールして好みのキーバインドにカスタマイズしています。

$ winget install powertoys
$ winget install ".NET Core"

現状の課題

この記事を執筆時点で早急にどうにかしなくては!と思っているのが、ssh-agentの設定です。現状はUbuntu上にkeychainをインストールして利用していますが、 VSCode起動時にパスフレーズを毎回聞かれたり、VSCodeのgit拡張からpushできずにVSCodeのターミナルからリモートリポジトリの操作をしています。 Windows側でssh-agentの設定をする方法があるみたいなので、近々試してみようと思っています。

最後に

十数年ぶりのWindowsに戸惑い多少ハマったりもしましたが、思っていた以上に簡単にWindowsでの開発環境を手に入れることができました。 みなさんも、機会があればWindowsでWSL2を利用した開発環境の構築をしてみるのもよいのではないでしょうか?