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

Kadai2 by @int128 #21

Open
wants to merge 3 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
1 change: 1 addition & 0 deletions kadai2/int128/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/kadai2
13 changes: 13 additions & 0 deletions kadai2/int128/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Copyright 2018 Hidetake Iwata

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
25 changes: 25 additions & 0 deletions kadai2/int128/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.PHONY: all test doc showReaderWriter

all: kadai2

kadai2: *.go */*.go
go build -o kadai2

test: *.go */*.go
go test -v -cover ./...

doc: *.go */*.go
godoc -ex cmd/github.com/gopherdojo/dojo2/kadai2/int128/images
godoc -ex cmd/github.com/gopherdojo/dojo2/kadai2/int128/options

showReaderWriterImplements:
cd `go env GOROOT`/src && \
find . -name '*.go' -and -not -name '*_test.go' -and -not -path '*/internal/*' -and -not -path '*/vendor/*' | \
xargs egrep -R 'func \(\w+ \*?[A-Z]\w+\) (Read|Write)\(\w+ \[\]byte\)' | \
column -t -s:

showReaderWriterRefs:
cd `go env GOROOT`/src && \
find . -name '*.go' -and -not -name '*_test.go' -and -not -path '*/internal/*' -and -not -path '*/vendor/*' | \
xargs egrep -R 'func .* io.(Reader|Writer)' | \
column -t -s:
170 changes: 170 additions & 0 deletions kadai2/int128/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# kadai2

See also https://github.com/gopherdojo/dojo2/tree/kadai1-int128/kadai1/int128.


## `io.Reader` と `io.Writer`

`io.Reader` と `io.Writer` はストリームの読み書きを行うためのインタフェースで、Javaにおける `InputStream` や `OutputStream` に相当する。

### 標準パッケージにおける利用

Go 1.10では `io.Reader` と `io.Writer` は以下のように定義されている。

```go
package io

type Reader interface {
Read(p []byte) (n int, err error)
}

type Writer interface {
Write(p []byte) (n int, err error)
}
```

Go 1.10の標準パッケージでは16個の構造体が `Read([]byte)` メソッドを実装している。
また、15個の構造体が `Write([]byte)` メソッドを実装している。
(テストコードおよび `internal` パッケージを除く)

具体的には以下のメソッドが存在する。

```go
% make showReaderWriterImplements
./bufio/bufio.go func (b *Reader) Read(p []byte) (n int, err error) {
./bufio/bufio.go func (b *Writer) Write(p []byte) (nn int, err error) {
./crypto/cipher/io.go func (r StreamReader) Read(dst []byte) (n int, err error) {
./crypto/cipher/io.go func (w StreamWriter) Write(src []byte) (n int, err error) {
./crypto/tls/conn.go func (c *Conn) Write(b []byte) (int, error) {
./crypto/tls/conn.go func (c *Conn) Read(b []byte) (n int, err error) {
./compress/flate/deflate.go func (w *Writer) Write(data []byte) (n int, err error) {
./compress/gzip/gzip.go func (z *Writer) Write(p []byte) (int, error) {
./compress/gzip/gunzip.go func (z *Reader) Read(p []byte) (n int, err error) {
./compress/zlib/writer.go func (z *Writer) Write(p []byte) (n int, err error) {
./strings/reader.go func (r *Reader) Read(b []byte) (n int, err error) {
./strings/builder.go func (b *Builder) Write(p []byte) (int, error) {
./net/net.go func (v *Buffers) Read(p []byte) (n int, err error) {
./net/http/httptest/recorder.go func (rw *ResponseRecorder) Write(buf []byte) (int, error) {
./archive/tar/writer.go func (tw *Writer) Write(b []byte) (int, error) {
./archive/tar/reader.go func (tr *Reader) Read(b []byte) (int, error) {
./bytes/buffer.go func (b *Buffer) Write(p []byte) (n int, err error) {
./bytes/buffer.go func (b *Buffer) Read(p []byte) (n int, err error) {
./bytes/reader.go func (r *Reader) Read(b []byte) (n int, err error) {
./io/io.go func (l *LimitedReader) Read(p []byte) (n int, err error) {
./io/io.go func (s *SectionReader) Read(p []byte) (n int, err error) {
./io/pipe.go func (r *PipeReader) Read(data []byte) (n int, err error) {
./io/pipe.go func (w *PipeWriter) Write(data []byte) (n int, err error) {
./math/rand/rand.go func (r *Rand) Read(p []byte) (n int, err error) {
./log/syslog/syslog.go func (w *Writer) Write(b []byte) (int, error) {
./mime/multipart/multipart.go func (p *Part) Read(d []byte) (n int, err error) {
./mime/quotedprintable/writer.go func (w *Writer) Write(p []byte) (n int, err error) {
./mime/quotedprintable/reader.go func (r *Reader) Read(p []byte) (n int, err error) {
./os/file.go func (f *File) Read(b []byte) (n int, err error) {
./os/file.go func (f *File) Write(b []byte) (n int, err error) {
./text/tabwriter/tabwriter.go func (b *Writer) Write(buf []byte) (n int, err error) {
```

メソッドの役割をまとめるとおおよそ以下のようになる。

- ファイルの読み書き
- ネットワーク通信
- 暗号化、復号
- ファイルの圧縮、展開(ZIP/TAR)
- MIMEエンコード、デコード
- バイト配列や文字列の処理
- 行指向やトークン分割の処理

このように、Goの標準パッケージでは入出力に関わるインタフェースが抽象化されていることが分かる。

### 抽象化の利点

入出力に関わるインタフェースを抽象化することで、コードをシンプルに保ちながら拡張性を持たせることができる。

例えば、 `image/jpeg` パッケージでは以下のメソッドが定義されている。

```go
func Decode(r io.Reader) (image.Image, error) {}
```

ローカルにあるJPEGファイルを読み込みたい場合は `os.Open()` の戻り値を渡せばよい。

```go
func Example_io_Reader_File() {
f, err := os.Open("image.jpg")
if err != nil {
panic(err)
}
defer f.Close()
img, err := jpeg.Decode(f)
if err != nil {
panic(err)
}
fmt.Printf("size=%+v", img.Bounds())
// Output: size=(0,0)-(1000,750)
}
```

また、リモートにあるJPEGファイルを読み込みたい場合は `http.Get()` の戻り値を渡せばよい。

```go
func Example_io_Reader_HTTP() {
resp, err := http.Get("https://upload.wikimedia.org/wikipedia/commons/b/b2/JPEG_compression_Example.jpg")
if err != nil {
panic(err)
}
defer resp.Body.Close()
img, err := jpeg.Decode(resp.Body)
if err != nil {
panic(err)
}
fmt.Printf("size=%+v", img.Bounds())
// Output: size=(0,0)-(1000,750)
}
```

もちろん、独自に定義した型を渡すこともできる。

```go
type DummyReader struct{}

func (r *DummyReader) Read(p []byte) (int, error) {
return 0, io.EOF
}

func Example_io_Reader_DummyReader() {
jpeg.Decode(&DummyReader{})
}
```

このように、インタフェースによる抽象化を行うことで、JPEGデータがローカルにある場合でもリモートにある場合でも同じメソッドを使うことができる。

もし、インタフェースが使えない場合は、以下のように具象型ごとに関数を定義することになる。

```go
func DecodeFile(f *os.File) (image.Image, error) {}
func DecodeHTTPResponseBody(r /* レスポンスボディ型 */) (image.Image, error) {}
func DecodeZIPFile(f *zip.File) (image.Image, error) {}
```

これでは具象型が増えるたびに関数を定義する必要があり、冗長なコードが増えてしまう。
また、標準パッケージの外側で独自に定義した型を受け取ることができない問題がある。


## kadai1のリファクタリングとテスト

前回の課題1でほとんどのテストコードを書いていたため、課題2では `main_test.go` を追加しました。


## 課題2

> io.Readerとio.Writerについて調べてみよう
>
> - 標準パッケージでどのように使われているか
> - io.Readerとio.Writerがあることでどういう利点があるのか具体例を挙げて考えてみる
>
> 1回目の宿題のテストを作ってみて下さい
>
> - テストのしやすさを考えてリファクタリングしてみる
> - テストのカバレッジを取ってみる
> - テーブル駆動テストを行う
> - テストヘルパーを作ってみる
46 changes: 46 additions & 0 deletions kadai2/int128/images/conversion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package images

import (
"fmt"
"os"
"path/filepath"
"strings"
)

// Conversion represents an image conversion between given formats.
type Conversion struct {
Decoder Decoder
Encoder Encoder
DestinationExt string
}

// ReplaceExt returns filename replaced the extension with DestinationExt.
// For example, if `DestinationExt` is `png`, `ReplaceExt("hello.jpg")` will return `"hello.png"`.
func (c *Conversion) ReplaceExt(filename string) string {
tail := filepath.Ext(filename)
head := strings.TrimSuffix(filename, tail)
return fmt.Sprintf("%s.%s", head, c.DestinationExt)
}

// Do converts the source file to destination.
// `source` and `destination` must be file path.
func (c *Conversion) Do(source string, destination string) error {
r, err := os.Open(source)
if err != nil {
return fmt.Errorf("Error while opening source file %s: %s", source, err)
}
defer r.Close()
m, err := c.Decoder.Decode(r)
if err != nil {
return fmt.Errorf("Error while decoding file %s: %s", source, err)
}
w, err := os.Create(destination)
if err != nil {
return fmt.Errorf("Error while opening destination file %s: %s", destination, err)
}
defer w.Close()
if err := c.Encoder.Encode(w, m); err != nil {
return fmt.Errorf("Error while encoding to file %s: %s", destination, err)
}
return nil
}
56 changes: 56 additions & 0 deletions kadai2/int128/images/conversion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package images

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

func ExampleConversion_ReplaceExt() {
conversion := &Conversion{DestinationExt: "png"}
destination := conversion.ReplaceExt("hello.jpg")
fmt.Println(destination)
// Output: hello.png
}

func ExampleConversion_Do() {
// Create a JPEG image and plot a pixel
jpegImage := image.NewRGBA(image.Rect(0, 0, 100, 200))
jpegFile, err := ioutil.TempFile("", "jpeg")
if err != nil {
panic(err)
}
defer jpegFile.Close()
defer os.Remove(jpegFile.Name())
if err := jpeg.Encode(jpegFile, jpegImage, nil); err != nil {
panic(err)
}

// Convert from JPEG to PNG
conversion := &Conversion{
Decoder: &JPEG{},
Encoder: &PNG{},
}
source := jpegFile.Name()
destination := conversion.ReplaceExt(source)
if err := conversion.Do(source, destination); err != nil {
panic(err)
}

// Read the PNG image
pngFile, err := os.Open(destination)
if err != nil {
panic(err)
}
defer pngFile.Close()
defer os.Remove(pngFile.Name())
pngImage, err := png.Decode(pngFile)
if err != nil {
panic(err)
}
fmt.Printf("size=%+v", pngImage.Bounds().Size())
// Output: size=(100,200)
}
73 changes: 73 additions & 0 deletions kadai2/int128/images/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package images

import (
"image"
"image/gif"
"image/jpeg"
"image/png"
"io"
)

// Decoder transforms binary to an image.
type Decoder interface {
Decode(io.Reader) (image.Image, error)
}

// Encoder transforms an image to binary.
type Encoder interface {
Encode(io.Writer, image.Image) error
}

// AutoDetect represents auto-detect.
type AutoDetect struct{}

// Decode automatically detects format and decodes the binary.
func (f *AutoDetect) Decode(r io.Reader) (image.Image, error) {
m, _, err := image.Decode(r)
return m, err
}

// JPEG represents JPEG format.
type JPEG struct {
Options jpeg.Options
}

// Decode transforms the JPEG binary to image.
func (f *JPEG) Decode(r io.Reader) (image.Image, error) {
return jpeg.Decode(r)
}

// Encode transforms the JPEG image to binary.
func (f *JPEG) Encode(w io.Writer, m image.Image) error {
return jpeg.Encode(w, m, &f.Options)
}

// PNG represents PNG format.
type PNG struct {
Options png.Encoder
}

// Decode transforms the PNG binary to image.
func (f *PNG) Decode(r io.Reader) (image.Image, error) {
return png.Decode(r)
}

// Encode transforms the PNG image to binary.
func (f *PNG) Encode(w io.Writer, m image.Image) error {
return f.Options.Encode(w, m)
}

// GIF represents GIF format.
type GIF struct {
Options gif.Options
}

// Decode transforms the GIF binary to image.
func (f *GIF) Decode(r io.Reader) (image.Image, error) {
return gif.Decode(r)
}

// Encode transforms the GIF image to binary.
func (f *GIF) Encode(w io.Writer, m image.Image) error {
return gif.Encode(w, m, &f.Options)
}
2 changes: 2 additions & 0 deletions kadai2/int128/images/images.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package images provides image conversion between various formats.
package images
Loading