Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tsuchinaga / 課題2 #13

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions kadai2/tsuchinaga/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## for IntelliJ
.idea
*.iml

testdata/**/*.converted.*
cover.out
100 changes: 100 additions & 0 deletions kadai2/tsuchinaga/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# 課題2 tsuchinaga

## 課題
スライドより転載

* io.Readerとio.Writerについて調べてみよう
* 標準パッケージでどのように使われているか
* io.Readerとio.Writerがあることで
どういう利点があるのか具体例を挙げて考えてみる
* 1回目の課題のテストを作ってみて下さい
* テストのしやすさを考えてリファクタリングしてみる
* テストのカバレッジを取ってみる
* テーブル駆動テストを行う
* テストヘルパーを作ってみる


### io.Readerとio.Writerについて調べてみよう

`io.Reader` と `io.Writer` を実装している代表的なのはやはり `os.File` だと思う。

`os.File` には `os.Stdin` 、 `os.Stdout` 、 `os.Stderr` がある。
これらはそれぞれ、標準入力、標準出力、標準エラーにあたり、誰もが入出力先にしたことがあると思う。

例えば、「出力先を標準出力からログファイルにかえたいなー」というときに、
`io.Writer` がなければ新たに出力用の処理を用意して上げる必要があったり、
標準ライブラリが `io.Writer` を出力先に指定していなければ、標準ライブラリと同じことをするのに自前で用意する必要があったりする。
さらに、「標準出力とログファイルの両方に出したいなー」みたいなときに、それぞれを同じように扱えるようにしていないと処理を作ることも難しい。

読み込みの代表例として、 `bufio.Scanner` をあげる。

競技プログラミングで大きな入力を受け取るときや、サイズの大きいファイルを読み込むときにお世話になる。
※ 他にも1単語ずつじゃなくて、1行ずつ読むときとかにも重宝する

この `Scanner` を生成する関数である `NewScanner` は下記のようになっている。
```go
// NewScanner returns a new Scanner to read from r.
// The split function defaults to ScanLines.
func NewScanner(r io.Reader) *Scanner {
return &Scanner{
r: r,
split: ScanLines,
maxTokenSize: MaxScanTokenSize,
}
}
```
引数が `io.Reader` となっている。
これは標準入力だろうが、ファイルだろうが、HTTPリクエストのBodyだろうが、なんでもかんでも同じように扱える。

同じような処理をいっぱい作らなくていい以外に、モックを作りやすいというメリットもある。(interfaceのメリットかも)

テストコードを書いていると、ここなんか適当なもので置き換えたいなーというのは頻繁にある。
そんな時に構造体に依存していたりすると置き換えることができず、密結合が起きてしまい、テストしづらくなる。
そこをinterfaceに依存するようにしていると、置き換えることが容易になる。


### 1回目の課題のテストを作ってみて下さい

リファクタリングの域をこえてるきもするけど、いつもやってるようにTODO出してテストを作っていく
課題1の時よりも責務を意識してTODOを分ける

* [x] ディレクトリが指定でき、その配下のディレクトリとファイルを再帰的に探索し、変換前として指定されたフォーマットの画像ファイルを変換後として指定されたフォーマットの画像ファイルに変換する
* [x] コマンドのパラメータを指定する
* [x] ベースのディレクトリを指定する
* [x] 必須
* [x] 変換前のファイルフォーマットを指定する
* [x] 任意
* [x] デフォルトjpeg
* [x] 許容されるのはjpeg/png
* [x] 変換後のファイルフォーマットを指定する
* [x] 任意
* [x] デフォルトpng
* [x] 許容されるのはjpeg/png
* [x] 変換前と同じフォーマットの指定は不可
* [x] 指定されたディレクトリの配下のディレクトリとファイルの一覧を取得する
* [x] 配下にディレクトリがあればさらに読み込めるようにする
* [x] 指定されたパスがディレクトリでなければ中止
* [x] ディレクトリかを判断する
* [x] ファイルの画像フォーマットを取得する
* [x] ファイルが画像なら画像フォーマットを取得する
* [x] ファイルが画像でなければフォーマットは取得できないので中止
* [x] ファイルフォーマットを変換する


#### カバレッジ
```bash
$ go tool cover -func=cover.out
github.com/gopherdojo/dojo8/kadai2/tsuchinaga/imgconv/imgconv.go:27: NewIMGConverter 100.0%
github.com/gopherdojo/dojo8/kadai2/tsuchinaga/imgconv/imgconv.go:42: Do 93.3%
github.com/gopherdojo/dojo8/kadai2/tsuchinaga/imgconv/imgconv.go:74: NewConverter 100.0%
github.com/gopherdojo/dojo8/kadai2/tsuchinaga/imgconv/imgconv.go:90: IsDir 100.0%
github.com/gopherdojo/dojo8/kadai2/tsuchinaga/imgconv/imgconv.go:99: GetIMGType 90.0%
github.com/gopherdojo/dojo8/kadai2/tsuchinaga/imgconv/imgconv.go:118: DirFileList 100.0%
github.com/gopherdojo/dojo8/kadai2/tsuchinaga/imgconv/imgconv.go:137: Convert 69.2%
github.com/gopherdojo/dojo8/kadai2/tsuchinaga/validation/validation.go:4: NewValidator 100.0%
github.com/gopherdojo/dojo8/kadai2/tsuchinaga/validation/validation.go:19: IsValidDir 100.0%
github.com/gopherdojo/dojo8/kadai2/tsuchinaga/validation/validation.go:26: IsValidFileType 100.0%
github.com/gopherdojo/dojo8/kadai2/tsuchinaga/validation/validation.go:31: IsValidSrc 100.0%
github.com/gopherdojo/dojo8/kadai2/tsuchinaga/validation/validation.go:36: IsValidDest 100.0%
total: (statements) 86.1%
```
5 changes: 5 additions & 0 deletions kadai2/tsuchinaga/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/gopherdojo/dojo8/kadai2/tsuchinaga

go 1.14

require golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
2 changes: 2 additions & 0 deletions kadai2/tsuchinaga/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
181 changes: 181 additions & 0 deletions kadai2/tsuchinaga/imgconv/imgconv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package imgconv

import (
"fmt"
"image"
"image/jpeg"
"image/png"
"io/ioutil"
"os"
"path"

"golang.org/x/xerrors"
)

var (
ReadDirError = xerrors.New("read dir error")
FileStatError = xerrors.New("file stat error")
OpenFileError = xerrors.New("open file error")
CloseFileError = xerrors.New("close file error")
NotImageError = xerrors.New("not image error")
ReadImageError = xerrors.New("read image error")
CreateDestinationFileError = xerrors.New("create destination file error")
EncodeImageError = xerrors.New("encode image error")
)

// NewIMGConverter - 新しい画像変換の生成
func NewIMGConverter() IMGConverter {
return &imgConverter{converter: NewConverter()} // converterを外からもらってもいいけど、使い方は決まってるので決め打ちで
}

// IMGConverter - 画像変換のユースケースのインターフェース
type IMGConverter interface {
Do(path, src, dest string) chan error
}

// imgConverter - 画像変換のユースケース
type imgConverter struct {
converter Converter
}

// Do - 指定されたディレクトリから再帰的にたどりながらファイルを変換する
func (c *imgConverter) Do(path, src, dest string) chan error {
ch := make(chan error)

go func() {
defer close(ch)

dirs, files, err := c.converter.DirFileList(path)
if err != nil {
ch <- err
return
}

// 非同期で再帰的にディレクトリをたどる
for _, dir := range dirs {
dch := c.Do(dir, src, dest)
for err := range dch {
ch <- err
}
}

// ファイルを変換する
for _, file := range files {
if err := c.converter.Convert(file, src, dest); err != nil {
ch <- err
}
}
}()

return ch
}

// NewConverter - 新しいConverterを生成する
func NewConverter() Converter {
return &converter{}
}

// converter - 変換処理とそれに付随する処理をもつサービスのインターフェース
type Converter interface {
IsDir(path string) (bool, error)
GetIMGType(path string) (_ string, err error)
DirFileList(filePath string) ([]string, []string, error)
Convert(filePath string, src string, dest string) (err error)
}

// converter - 変換処理とそれに付随する処理をもつサービス
type converter struct{}

// IsDir - pathがディレクトリかどうか
func (c converter) IsDir(path string) (bool, error) {
fi, err := os.Stat(path)
if err != nil {
return false, xerrors.Errorf("%+v: %w", err, FileStatError)
}
return fi.IsDir(), nil
}

// GetIMGType - 引数のファイルの画像フォーマットを取得する
func (c converter) GetIMGType(path string) (_ string, err error) {
f, err := os.Open(path)
if err != nil { // 開けない
return "", OpenFileError
}
defer func() {
if closeErr := f.Close(); closeErr != nil {
err = xerrors.Errorf("%+v: %w", closeErr, CloseFileError)
}
}()

_, format, err := image.DecodeConfig(f)
if err != nil { // 画像じゃない
return "", NotImageError
}
return format, nil
}

// DirFileList - 引数直下のディレクトリとファイルの配列を返す
func (c converter) DirFileList(filePath string) ([]string, []string, error) {
infos, err := ioutil.ReadDir(filePath)
if err != nil {
return nil, nil, xerrors.Errorf("%+v(filePath: %s): %w", err, filePath, ReadDirError)
}

dirs, files := make([]string, 0), make([]string, 0)
for _, info := range infos {
jp := path.Join(filePath, info.Name())
if info.IsDir() {
dirs = append(dirs, jp)
} else {
files = append(files, jp)
}
}
return dirs, files, nil
}

// Convert - 画像変換
func (c converter) Convert(filePath string, src string, dest string) (err error) {
if ft, err := c.GetIMGType(filePath); err != nil {
return err
} else if ft != src {
return nil
}

f, err := os.Open(filePath)
if err != nil { // 開けない
return xerrors.Errorf("%+v: %w", err, OpenFileError)
}
defer func() {
if closeErr := f.Close(); closeErr != nil {
err = xerrors.Errorf("%+v: %w", closeErr, CloseFileError)
}
}()

img, _, err := image.Decode(f)
if err != nil {
return xerrors.Errorf("%+v: %w", err, ReadImageError)
}

newFilePath := fmt.Sprintf("%s.%s.%s", filePath, "converted", dest)
o, err := os.Create(newFilePath)
if err != nil {
return xerrors.Errorf("%+v: %w", err, CreateDestinationFileError)
}
defer func() {
if closeErr := o.Close(); closeErr != nil {
err = xerrors.Errorf("%+v: %w", closeErr, CloseFileError)
}
}()

switch dest {
case "jpeg":
if err = jpeg.Encode(o, img, nil); err != nil {
return xerrors.Errorf("%+v: %w", err, EncodeImageError)
}
case "png":
if err = png.Encode(o, img); err != nil {
return xerrors.Errorf("%+v: %w", err, EncodeImageError)
}
}
return nil
}
Loading