Gobble up pudding

プログラミングの記事がメインのブログです。

MENU

プログラムのインターフェースは必要か

f:id:fa11enprince:20200628230343j:plain
ふと、いろんな記事を見ていて、インターフェースは必要かっていうのがあった。

Java インターフェース メリット わからない - 社内se × プログラマ × ビッグデータ

どういうものかは分かりますが、メリットについては何も分からないです。
処理を具体的に書かないと、何の役にも立たないのに、何でそんなことをするのか?という疑問が残ります。

というような記載を見つけました。 こういう風に思うのは正常だと思います。
いや、当然必要だけど、JavaとかJavaとかJavaとか過剰に使いすぎてるのでは?と思ったところがあったので…。RubyとかPythonってinterfaceないっすよね(そのかわり別にいろいろ後付けで実装差し替えなど動的言語ならではのことができますが…)。 なんかほら、StrutsでDIが積極的に取り入れたときに、あの汎用性重視のやりすぎ感。それに近いものを感じるわけです。結局歴史的には設定より規約(CoC)に流れていき、汎用性が高いけれども一方でとても面倒なXML地獄、いい加減、毎度おんなじテンプレみたいな呪文をひたすら書くということから解放されたのだ(しかも間違ったら意味不明なミスに遭遇するし、XMLなので静的チェックもかけにくい)…。

自分が思うにインターフェースにするメリットは

  • テストクラスを書くときに簡単に実装をダミーに差し替えれる
  • 大規模開発しているときにとりあえずこれをこういう形で呼べばいいというのを利用側に宣言できる

というのだけに尽きると思います。
ただ、フレームワークを作っている側だともっと恩恵があって、
これ以上にメリットがあってあとで丸っと実装を差し替えたりしても後方互換性がとりあえずインターフェースだけ変えなければ保たれるとかあると思うのですが…

Javaの話でいえば特に昔のSpring Frameworkとかの慣習だと何でもかんでも実装とインターフェースを分けて書きますが、正直アプリレベルの上位レイヤーでそれをやるとやりすぎ感があったりするような気がします。ぶっちゃけフレームワークの上位のアプリレベルだと別にインタフェース書かなくてもいいんじゃ?という気がします。テストフレームワーク(JUnitとかその他)でもMockでいろいろ差し替えようと思えば差し替えられるので。あれですね特にServiceクラスで見るFooServiceFooServiceImplとか。 正直、インターフェース書いてオーバーライドするのがだるい…アプリレベルならその手間があるならインターフェース書かなくてもいいんじゃ的な…。メリットよりだるさが上回ります。 小規模~中規模とかで自分が全部掌握しているようなプロジェクトだとインターフェースはほぼ意味をなさないんじゃないかと。
一方抽象クラスでポリモーフィックに作ろうとするとabstractメソッドは便利なんですがinterfaceってなんか中途半端というか。

といいつつ、Spring Bootでは慣習に倣いServiceとServieImplは分けています…。辞めようかなと思った今日この頃。なんでかって?テストクラスをpackage privateにすれば簡単に差し替えれるから。 そもそもSpringでDIする場合は@Autowiredでそのクラスを書き換えれば疎結合は維持されるので(ただし、呼び出し元の書き換えが発生しますが…)、余計メリットを感じないわけです。 そもそもクラスの名前がイケてなかった。あとで書き換えたい…ってときは結局インターフェースにしても意味ないですので…ということで、アプリなどの上位レイヤーばかり書いていると、メリット薄いかもしれないです…。 フレームワークはフレームワークでインターフェースないとしんどそうですが、処理を追うときに、実際にはどの実装使ってんのよ?っていうのが追うの辛いという…。

AngularとjQuery/jQuery UIを組み合わせる

f:id:fa11enprince:20180526233100j:plain AngularとjQuery/jQuery UIを組み合わせるのは何か間違ってる気がしますが、
Angularの部品が足りなくて、どうしても使いたいことが起きることがあるかと思います(たぶん…)。
ここは意識低い系の方法を紹介します。
(意識高い系の方法は知識不足でちょっとわからなかった)。一応補足に記載。
ここで紹介する方法のデメリットはTypeScriptを使ってるのにjQueryの型がなくなってしまうことです(てかそれで別に困らない)。 こういうシチュエーションがそれなりにあると思うのに、
jQuery排他主義の人が多いのか(自分もどちらかといえばそう) 情報が錯綜しているのでまとめました。

jQueryをインストールする

これは普通にnpmからインストールします

$ npm install --save-dev jquery

jQuery UIをインストールする

公式のは1.12.1で止まっていて、いつからかnpmで提供されるのは部品ごとに細かく分かれたらしく、 Angularから使うにはwebpackをいじらないといけない模様。 ただ、jquery-ui-distっていうのを作ってくれている人がいて、それを使えばnpmからイケるのだが、 ちょっと怪しいので、フツーに公式からzipを落として配置することにする。 https://jqueryui.com/download/
から最新版をデフォルトの設定で落として、 src/assets/vendor/jquery-ui とか作ってあげてそこに以下の解凍した中身を放り込む

external/
images/
AUTHORS.txt
...
jquery-ui.theme.min.css
LICENSE.txt
package.json

angular.jsonにCSSとJSを登録する

 "projects": {
    "client": {
      "root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "architect": {
        "build": {
        ...
            "styles": [
              "src/assets/vendor/jquery-ui/jquery-ui.min.css",
              ...
            ],
            "scripts": [
              "node_modules/jquery/dist/jquery.min.js",
              "src/assets/vendor/jquery-ui/jquery-ui.min.js"
              ...
            ]
...

stylesscripts部分にJQueryとJQueryUIのCSSとJSを追加する。

使ってみる

あとはjQuery/jQuery UIを使っているところのTypeScriptで
declare var $: any;と書けばいいだけ。
この場合、import $ from 'jquery';はいりません。
ただし、型はない状態のままです。
これだけです。 どうしてもDOMを操作する場合は ngAfterViewInit()でやるとかありますが、基本はこれだけでOKです。

補足: 意識高い系の方法

おそらく、型を使う場合は、うまくいってませんが

$ npm install --save-dev @types/jquery
$ npm install --save-dev @types/jqueryui

とやってかつ

import $ from 'jquery';
import 'jqueryui';

とやればOKだと思われる...がこれだけではうまくいかない…(詳細不明)。 詳しい人教えてください。

ググると出てくる情報源

https://stackoverflow.com/questions/30623825/how-to-use-jquery-with-angular

Angular6に移行メモ

f:id:fa11enprince:20180628144634j:plain
Angular5.2からAngular6に移行したのでメモ

移行手順

https://update.angular.io/
で示されることをひたすらやっていく

  • Angular Version 5.2 -> 6.0

  • App Complexity Basic

  • ngUpgrade I use ngUpgrade no

  • Package Manager npm

Before Updating

httpモジュールからhttpClientモジュールへ

Switch from HttpModule and the Http service to HttpClientModule and the HttpClient service. HttpClient simplifies the default ergonomics (You don't need to map to json anymore) and now supports typed return values and interceptors. Read more on angular.io

→すでに実施済み

https://brianflove.com/2017/07/21/migrating-to-http-client/

                                                                                               
+import { HttpClient, HttpParams } from '@angular/common/http';                                                       
 import { Injectable } from '@angular/core';                                                                          
-import { Headers, Http, RequestOptions, Response, URLSearchParams } from '@angular/http';                            
+                                                                                                                     
 import { Observable } from 'rxjs/Observable';                                                                        
                                                                                                                      
import 'rxjs/add/observable/throw';                                                                                  
export class HogeService {                                                                                                       
                                                                                                                      
-    constructor(private http: Http) { }                                                                              
+    constructor(private httpClient: HttpClient) { }                                                                  
...                                                                                      
         locationCode: string,                                                                                        
         page: number,                                                                                                
         pageSize: number,                                                                                            
-        optionalParams: URLSearchParams,                                                                             
+        optionalParams: HttpParams,                                                                                  
     ): Observable<IPaginationPage<Hoge>> {                                                                    
-        const params = new URLSearchParams();                                                                                                                                 
-        params.set('size', `${pageSize}`);                                                                           
-        params.set('page', `${page}`);                                                                               
-        if (optionalParams != null && optionalParams.paramsMap.size !== 0) {                                         
-            params.appendAll(optionalParams);                                                                        
-        }                                                                                                            
-        const options = new RequestOptions({ search: params });                                                      
-        return this.http.get(this.hogeAllGetUrl, options).map(this.extractData)                               
+        let params: HttpParams = new HttpParams()                                                                                                                                  
+            .set('size', `${pageSize}`)                                                                              
+            .set('page', `${page}`);                                                                                 
+        optionalParams.keys().forEach((key) => {                                                                     
+            params = params.append(key, optionalParams.get(key));  // 注意:直観に反して戻り値を取らないと変わらない                        
+        });                                                                                                          
+        return this.httpClient.get<IPaginationPage<Hoge>>(this.hogeGetUrl, {params})                
+            .map(this.extractData)                                                                                   
             .catch((error) => Observable.throw(error.statusText));                                                   
     }                                                                                                                

これをやってなかったら多分これが一番大変。

Angular/CLIのアップデート

Update your Angular CLI globally and locally, and migrate the configuration to the new angular.json format by running the following:

npm install -g @angular/cli
npm install @angular/cli@latest
ng update @angular/cli

これで失敗する場合はたぶんNode.jsのバージョンを上げろと言ってきます。

Angularのcoreのアップデート

Update all of your Angular framework packages to v6, and the correct version of RxJS and TypeScript.

ng update @angular/core

Angular Materialのアップデート

After the update, TypeScript and RxJS will more accurately flow types across your application, which may expose existing errors in your application's typings

Update Angular Material to the latest version.

-> 使ってないのでやってない

After the Update

RxJSのlintを入れ替えてRxJSを新バージョンに

Remove deprecated RxJS 6 features using rxjs-tslint auto update rules. For most applications this will mean running the following two commands:

npm install -g rxjs-tslint
rxjs-5-to-6-migrate -p src/tsconfig.app.json # →失敗したが放置

ここからRxJSの直しにかかります。RxJSがバージョンアップしているので書き換えます。

 import { HttpClient, HttpParams } from '@angular/common/http';
 import { Injectable } from '@angular/core';

-import { Observable } from 'rxjs/Observable';
-
-import 'rxjs/add/observable/throw';
-import 'rxjs/add/operator/catch';
-import 'rxjs/add/operator/map';
+import { Observable, throwError } from 'rxjs';
+import { catchError, map } from 'rxjs/operators';
...
         return this.httpClient.get<IPaginationPage<Hoge>>(this.hogeGetUrl, {params})
-            .map(this.extractData)
-            .catch((error) => Observable.throw(error.statusText));
+            .pipe(
+                map(this.extractData),
+                catchError((error) => throwError(error.statusText)),
+            );
     }

Angular6へあげた効果

ビルド時間早くなった
200秒から60秒
バンドルサイズが減った
main.bundle.jsが1.1MBから800kB

以上で終わり。お疲れさまでした。

その後エラーになったことなど

AjaxでJSONをパラメータとしてPOSTする場合

これはAngular6の移行というよりhttpModuleからhttpClientModuleへ変えたときに問題になる話

npm installでnode-sassがエラーになる

node_modules\node-sass
> node scripts/install.js

module.js:549
    throw err;
    ^

Error: Cannot find module 'true-case-path'
    at Function.Module._resolveFilename (module.js:547:15)
    at Function.Module._load (module.js:474:25)
    at Module.require (module.js:596:17)
    at require (internal/module.js:11:18)

ngx-bootstrapがらみの問題

  • Accordion いつの間にかタイトル文字がクリック可能になってSubmit発火する→使うのやめる
  • Datepicker 下記の問題により、ヘッダの文字が崩れる

https://github.com/valor-software/ngx-bootstrap/issues/4443

// as of Angular 6 they set preserveWhitespaces to false from default.
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
(window as any).global = window;

polyfills.ts

/***************************************************************************************************
 * APPLICATION IMPORTS
 */
import 'global-shim';  // workaround
/**
 * Date, currency, decimal and percent pipes.
 * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
 */
import 'intl';  // Run `npm install --save intl`.

その他気になったこと

全然関係ないが、Angularのコンポーネントって少ないなーと思ってたんですが、 探したらそこそこあるんですね。

中でもすごいのがコレ

でもAngularって極力materialとかAngular Teamが提供しているもの以外依存しないほうがいろいろと幸せになれそうな気がする…

ちなみに僕はngx-bootstrapを使っています。

おまけ

Angular5からAngular6に移行した小さなサンプルを置きました

Angular5のmain.bundle.jsが重いのでSpring BootのGzip圧縮を試してみる

f:id:fa11enprince:20180611025013j:plain Angular6出ましたね!割と前に…
Angularを使っています。Angularは割といろいろ好きなところはあるのですが、
やはり、FullのSPAを作っているならまだしも、そうでないので、
趣味&実験でプロダクトにぶっこんだ側面もあるので、いろいろ困難があります。
まぁ、端的に言うと、作ろうとしているものに対しての選定ミス。
おとなしくVue.jsとかが向いていたと思います。
そこで今回、重大だ、という問題について書きます。

Angularのmain.bundle.のサイズでかい問題

Angularのmain.bundle.jsが重い問題。
貧弱なネットワークの中ではこれがとても問題になる。
1M超えのJSはやっぱりちょっときつい…
何とかしたい。Angular5を使っているのだがどうにもならない…。
なんででかくなるかはわかっている
jQuery, Momentを使っているからだ。
jQueryは対処しようがなく、使用しないしか対処法がない。
Momentはlocaleを絞ればよい。
それでもでかい…1MBを切るのが難しい。
いろいろ調べると--vendor-chunck=trueを付けるといいとかでてくるが、これは意味がない。
main.bundle.jsのかわりにvendor.bundle.jsのサイズがでかくなるからだ。
Angular6に実験で上げてみるも、あんまり効果ない&RxJSの書き換えがだるい…ので諦めました。
時間があったらバージョンアップしたい。

いろいろ調べてみる

やはりjQuery, Lodash, Momentあたりを使ってると部分的な対策はあるものの(特にMomentのlocaleとか)、
根本的な対策はないようだ。
そもそもそれらを使わないというのがベストなのだが、どうしようもない事情があるのも確か…。

https://github.com/angular/angular/issues/16850
によると…

元の文

" it shouldn't be expected to use gzip in order to get a realistic file size." If being lightweight is crucial to your project, you might want to consider trying out a view library like React or VueJs until angular properly supports server-side rendering and platform-server is documented. Unfortunately, since angular is a complete framework, its size is large and getting the porduction files to less than 100kb even with compression is not trivial [I've read some people experimenting with the closure compiler getting builds ~50kb in size]

" As I said, the servers I'm using do not support gzip" . The express compression middle-ware does the compression on the fly as a client requests the resources and does not need the server to support gzip files, meaning that the files you'll be uploading to your server are going to be the original files built using ng build command as-is, without being compressed.

翻訳

「(サーバがgzip圧縮に対応してないので)現実的なファイルサイズを得るためにgzipを使用することは期待できません。」 軽量であることがプロジェクトにとって重要なら、サーバサイドレンダリングやplatform-serverを適切にサポートしていることがドキュメントに書かれるまで、ReactやVueJsなどのViewライブラリを試すことを検討することをお勧めします。 残念ながら、Angularは完全なフレームワークなので、サイズは大きく、圧縮してもporductionファイルを100kb未満にすることは自明ではありません。 [私はclosureコンパイラを使って~50kbのサイズをにしたというのを読んだことがあります]

「私が言ったように、私が使用しているサーバはgzipをサポートしていません」 。 Expressのオンザフライで圧縮を行うミドルウェアがあるので、gzipファイルをサポートする必要がありません。つまり、サーバーにアップロードするファイルは圧縮されずにng buildのそのままということになります。

端的に言うと、それなりのbundleのサイズは覚悟しろってことか。

他にもSSRを使えというのがあるが、そもそもサーバサイドはExpressじゃなく、Javaなので使えない…
部分的に導入ってのもありはありだが、めんどくさい事がかなり増える。
そもそもExpressは好きじゃない(そういう問題ではない)。
JavaでSSRを行うものもGitHubに上がってるが、今後のバージョンアップに追随できるか謎なのでまっとうな方法でいきたい。しかし、このやりっぷりには感動した。
ちなみにJNIでV8をたたいてる……ものだった。
そのほかの方法としてはLazy Loadingか…。

ということで…
結論としてはGzip圧縮を試みようということになった。
幸い、使っているものはSpring Bootなので、Gzip圧縮を気軽に試せる。
もちろん、古き良きApacheにもモジュール追加でその方法は試せるが。

Gzip圧縮を使ってみる

まずはAOTビルド時のサイズ

Time: 119604ms
chunk {scripts} scripts.bundle.js (scripts) 398 kB [initial] [rendered]
chunk {0} main.bundle.js (main) 1.16 MB [initial] [rendered]
chunk {1} polyfills.bundle.js (polyfills) 169 kB [initial] [rendered]
chunk {2} styles.bundle.css (styles) 219 kB [initial] [rendered]
chunk {3} inline.bundle.js (inline) 1.36 kB [entry] [rendered]

SSDのそこそこのスペックのマシンだが11sかかってる…。そして1.16MB…
もちろん、RxJSとかのimportは極小になるように頑張っている

Spring Bootの公式サイトによると

server.compression.enabled=false # If response compression is enabled.

がデフォルトのことなので、

application.yamlを書き換える

server:
  compression:
    enabled: true
    mime-types=application/json,application/xml,text/html,text/xml,text/plain,application/javascript,text/css

Chromeの帯域制限で計測

に記載の方法で試してみる。 Fast3Gで計測

圧縮前
f:id:fa11enprince:20180709224402p:plain
圧縮後
f:id:fa11enprince:20180709230852p:plain
帯域を絞ると…だいぶ早くなってますね!

GZIPで返ってこないと少しハマった件

ちなみにですが、あれれ?gzipでレスポンス返ってきてない!!とおもってハマりました。 アンチウィルスのESETのせいでgzipで受け付けなかったようです。
f:id:fa11enprince:20180709230605p:plain
ここの設定をOFFにしないとダメでした(ファイヤーウォール全無効にするだけじゃダメでした)