※初期化時に渡すvalueは数字という前提です。
class AAA { constructor(value) { this.value = value; } asValueSeparatedByComma() { return String(this.value).replace( /(\d)(?=(\d\d\d)+(?!\d))/g, '$1,'); } doSomething() { } } class BBB { constructor(value) { this.value = value; } asValueSeparatedByComma() { return String(this.value).replace( /(\d)(?=(\d\d\d)+(?!\d))/g, '$1,'); } doSomethingElse() { } }
例えば、addCommaという数字を3桁区切りでカンマをつける全く同じメソッドが複数間のクラスに存在する場合、
親クラスを作ってあげるとか、そのメソッドを別ファイルに保存してインポートして使うとか、いろいろと選択肢はあると思うのですが、
適切な方法の判断の仕方がわかりません。
とりあえず、同じ処理が複数の場所に散らばってしまうのは、いざ、その該当処理を修正する際に修正箇所が増えるので好ましくないというのは分かります。
継承を採用する場合、依存関係が親と子で生まれたり、あとは後で変更したいみたいになったときに、手間がかかりそうな気もしますし、いろいろと考えることもありそうです。関数を単にインポートして使うのは楽ですが、なんかユーティリティークラスはoop的には好ましくないみたいなことを目にしたこともあり、どうしたものかと。
というか、使ってるredux自体が関数型プログラミングを採用してる感じですし、その時点で関数インポートして使ってたりもしますし尚更わけわからん。
どんな方法が良いのでしょうか!!!!
補足
https://www.google.co.jp/search?q=ユーティリティークラス+アンチパターン
追記
数字を扱うクラスを作って、それをコンポジションして使えば良いんですかね?
変更(2017/10/01 16:21)
カンマ区切りの処理がどういう処理か名前からわかりづらいというご指摘をmiyabiさんから受けたので、メソッド名を変更しました。
カンマ区切り用のメソッド引数に値を外から渡す書き方をしていましたが、意図していたものと違かったため、初期化時に渡される値をもとにカンマ区切りにした文字を返すように変更しました。
追記(2017/10/01 16:45)
miyabiさんのprototype拡張コードをお借りしつつ、こんなの試しに書いてみましたが、「wrappedClass.prototype.value」は上手く行かないのですね。 class AAA { constructor(value) { this.value = value; } doSomething() { console.log('hello'); } } function enhancer(wrappedClass) { Object.defineProperty( wrappedClass.prototype, 'asValueSeparatedByComma', {get: function(){return String(wrappedClass.prototype.value).replace( /(\d)(?=(\d\d\d)+(?!\d))/g, '$1,')}} ) return wrappedClass; } var enhancedClass = enhancer(AAA) var enhancedObject = new enhancedClass(5) console.log(enhancedObject.asValueSeparatedByComma); //undefined enhancedObject.doSomething() //'hello'
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
回答7件
0
ベストアンサー
###mixinまたはトレイト
親子関係といった継承を使った関係ではないクラス同士で共通の処理がある場合どうするかというと、よくある言語ではmixinやトレイトを使います。マイナーなaltJSの一つであるLiveScriptを使ったmixinの例を示しましょう。
LiveScript
1HasValueSeparatedByComma = 2 asValueSeparatedByComma: -> 3 String(this.value).replace( /(\d)(?=(\d\d\d)+(?!\d))/g, '$1,'); 4 5class AAA implements HasValueSeparatedByComma 6 (@value) -> 7 8 doSomething: -> 9 10class BBB implements HasValueSeparatedByComma 11 (@value) -> 12 13 doSomething: -> 14 15a = new AAA(1234) 16b = new BBB(5678) 17 18console.log(a.asValueSeparatedByComma()) 19console.log(b.asValueSeparatedByComma())
asValueSeparatedByComma
の実装が一つにまとまっているのがわかると思います。では、これを生のJavaScriptで書くとどうなるのかというと…今はできません。なぜなら、現在のJavaScript(ECMAScript)でmixinやトレイトを機能として提供していないからです。mixinを提供するための機能としてFirst-Class Protocolsが提案されていますが、現在stage 1(stage 4が正式採用)であり、まだまだ道のりは遠い状態です。
ただ、機能として提供していないからと言ってできないわけではありません。上のLiveScriptですと、prototypeにforで回しながら全て突っ込むという荒技を使って実現しています。ES2015+で再現するとなると次のような感じです。
JavaScript
1function importAll$(obj, src) { 2 for (const key in src) obj[key] = src[key]; 3 return obj; 4} 5 6const HasValueSeparatedByComma = { 7 asValueSeparatedByComma: function() { 8 return String(this.value).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,'); 9 } 10}; 11 12class AAA { 13 constructor(value) { 14 this.value = value; 15 } 16 17 doSomething() { 18 } 19} 20importAll$(AAA.prototype, HasValueSeparatedByComma); 21 22class BBB { 23 constructor(value) { 24 this.value = value; 25 } 26 27 doSomethingElse() { 28 } 29} 30importAll$(BBB.prototype, HasValueSeparatedByComma); 31 32a = new AAA(1234); 33b = new BBB(5678); 34 35console.log(a.asValueSeparatedByComma()); 36console.log(b.asValueSeparatedByComma());
これが良いのか悪いのかと問われると、微妙と思っています。そもそものやり方が悪いというよりも、JavaScriptはこういう設計が向いていない、こういう設計をできるように考えて作られていない、こういう設計をするには力不足である、という感じです。かつてJavaにもmixinがありませんでしたが、そのときと同じ匂いを感じています。
「ユーティリティークラスは悪」のように言われていますが、言語そのものの機能不足の所為で、ユーティリティークラスでも作らないと共通部分が書きにくいJavaの功罪だと思っています。それと同じで、まだクラスベースのオブジェクト指向をするには十分とは言えないJavaScriptにおいては、ユーティリティークラスでも良いんじゃ無いかと思っています。もし、それがどうしても嫌なら、altJSを使うしか無いでしょう。
上の話とは関係無い、別の方法を紹介します。善し悪しについては判断はしません。
###意図的に汎用なメソッド
AAA.prototype.asValueSeparatedByComma
は意図的に汎用な作りですので、そのまま流用可能です。
JavaScript
1class AAA { 2 constructor(value) { 3 this.value = value; 4 } 5 6 asValueSeparatedByComma() { 7 return String(this.value).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,'); 8 } 9 10 doSomething() { 11 } 12} 13 14class BBB { 15 constructor(value) { 16 this.value = value; 17 } 18 19 doSomethingElse() { 20 } 21} 22 23a = new AAA(1234); 24b = new BBB(5678); 25 26console.log(a.asValueSeparatedByComma()); 27console.log(AAA.prototype.asValueSeparatedByComma.call(b));
###Number拡張
Numberそのものを拡張します。asValueSeparatedByComma
自体がまとめられるわけではありませんが、複雑なコード部分を共通化できます。
JavaScript
1Number.prototype.toStringSeparatedByComman = function() { 2 return this.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,'); 3}; 4 5class AAA { 6 constructor(value) { 7 this.value = value; 8 } 9 10 asValueSeparatedByComma() { 11 return this.value.toStringSeparatedByComman(); 12 } 13 14 doSomething() { 15 } 16} 17 18class BBB { 19 constructor(value) { 20 this.value = value; 21 } 22 23 asValueSeparatedByComma() { 24 return this.value.toStringSeparatedByComman(); 25 } 26 27 doSomethingElse() { 28 } 29} 30 31a = new AAA(1234); 32b = new BBB(5678); 33 34console.log(a.asValueSeparatedByComma()); 35console.log(b.asValueSeparatedByComma());
投稿2017/10/01 11:24
編集2017/10/01 11:54総合スコア21733
0
静的関数
また、「リファクタリングを求めているわけではないのでコードを書く必要性を感じません」といわれそうですが、ケースバイケースだと思うので、コードを出してほしいと思います。
コードを出せば、前提条件を後出しする事がないからです。
例えば、addComma()
が静的関数であった場合、class AAA や class BBB に存在しなくても良いと考えられます。
JavaScript
1function addComma (numberString) { 2 return numberString.split(/(?=(?:\d{3})+$)/).join(); 3}
関数 addComma は class AAA, class BBB に依存せずに動作する為、そこに含めるべきではありません。
汎用性
2017/10/01 16:21の変更を受けて追記します。
関数名が変更(addComma -> asValueSeparatedByComma) されていますが、そこは重要ではなく、addComma では存在した仮引数 value
がなくなっています。
変更前の addComma では静的関数を予想させるコードでしたが、this.value
に依存するメソッドへ変質したことで状況は大きく変わりました。
共通処理のメンテナンスコストを下げるには共通処理を外へ追い出す必要があります。
共通処理を外へ追い出すという事は依存関係が出来るという事です。
どうやっても依存関係が出来るのなら、「汎用性が高い方法」を採用するのがベストだと考えます。
一つは、参照透過性です。
ご質問のコードでは this.value
に依存している為、参照透過性がないコードですが、this.foo
でも this.piyo
でも再利用できるコードが好ましいでしょう。
JavaScript
1'use strict'; 2function insertCommaDelimiter (numberString) { 3 return String(numberString).replace(/(\d+)(.\d+)?/, function (subString, capture1, capture2) { 4 capture1 = capture1.split(/(?=(?:\d{3})+$)/).join(); 5 return capture2 ? capture1 + capture2.replace(/(\d{3})(?=\d)/, '$1,') : capture1; 6 }); 7} 8 9class AAA { 10 constructor (value) { 11 this.value = value; 12 } 13 14 asValueSeparatedByComma () { 15 return insertCommaDelimiter(this.value); 16 } 17} 18 19class BBB { 20 constructor (foo) { 21 this.foo = foo; 22 } 23 24 asValueSeparatedByComma () { 25 return insertCommaDelimiter(this.foo); 26 } 27} 28 29console.log(new AAA(12345678).asValueSeparatedByComma()); // "12,345,678" 30console.log(new BBB('私の所持金額は43421円です。').asValueSeparatedByComma()); // "私の所持金額は43,421円です。"
更に汎用性を上げると、挿入されるデリミタはカンマでなくても良く、任意の文字数単位でデリミタを挿入できる設計が考えられます。
JavaScript
1function insertDelimiterByNumberString (numberString, digit, delimiter) { 2 var digit = Number(digit), 3 integerPart = new RegExp('(?=(?:\d{' + digit + '})+$)'), 4 decimalPart = new RegExp('(\d{' + digit + '})(?=\d)', 'g'), 5 delimiter = arguments.length < 3 ? ',' : String(delimiter); 6 7 return String(numberString).replace(/(\d+)(.\d+)?/g, function (subString, capture1, capture2) { 8 capture1 = capture1.split(integerPart).join(delimiter); 9 return capture2 ? capture1 + capture2.replace(decimalPart, '$1,') : capture1; 10 }); 11} 12 13console.log(insertDelimiterByNumberString(12345678.42133212, 3)); // "12,345,678.421,332,12" 14console.log(insertDelimiterByNumberString(180012, 2, ':')); // "18:00:12"
もう一つの方向性としては、汎用的なクラスを作る事です。
例えば、moment.js は Date
を操る事に特化したライブラリですが、カンマ区切りの数値を作る処理も何らかの汎用性の高いクラスの一部とします。
class AAA, class BBB は作った class との間に依存関係が出来ますが、汎用性が高いクラスであれば気にならないかもしれません。
ただし、この方法はたった一つの機能が欲しいがために重い class をインポートする事にも繋がるので、設計方針次第では採用しがたいと思います。
いずれにしても、再利用性を高める為には、可能な限り、汎用性が高い設計にする必要があると考えます。
私の書いた例は一例に過ぎず、最終的には「汎用性をどの方向に追求するか」というポリシーの問題になります。
更新履歴
- 2017/10/01 18:26 「汎用性」を追記
Re: hayatomo さん
投稿2017/10/01 07:01
編集2017/10/01 09:26総合スコア18156
0
要は、AOPをjavascriptで、という話だと思いますので、将来的にはデコレータで処理したくなる案件ですかね……?
きれいで読みやすいJavaScriptを書く デコレーターの基本を先取り - WPJ
投稿2017/10/06 08:46
総合スコア35865
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
カンマを付与するって行為自体は、項目の設定によるから共通化は難しいですよ。
私なら、画面の項目に属性(3桁カンマ表示)を付与しておきますね。
画面の属性処理みたいなクラスに集めておけばよいのではないでしょうか。
クライアント側に処理を持たせれば、サーバの処理を分散でき、汎用性も増しますね。
少なくとも、処理の中でカンマを付与して、
純粋でないデータを渡すようなことはしたくありません。
追記
画面の項目または、モデルクラスの各プロパティに属性を付与すればいいです。
属性を付与していない言語であるなら、辞書みたいなのを作って管理しますかね。
投稿2017/10/04 22:26
編集2017/10/04 22:43総合スコア110
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
退会済みユーザー
2017/10/04 23:14
0
私の場合なんとなく同様のケースは無いだろうなと思いつつ、しばらく考えるのに時間がかかりましたがどういうことか思い当たりましたので書きます。読んでみるとOOPをそれほど大事に考えていないことが判ると思います。OOPが大事というなら的外れな回答になっている点をご了承下さい。
クラスの設計において、私の場合は3層に分けて設計します。
1つ目は、システム内部の足回りで、ファイルの入出力をしたり、DBの接続をしたりSQLを管理して永続化したオブジェクトの取り出しを行ったり、する部分です。泥臭い設定部分を受け持ちます。
2つ目は、外部へのインターフェイスでこれにはWebなどのUIも含まれます。多様なインターフェイスで同様の機能が提供出来るようにインターフェイスに依存する部分はここで受け持ち泥臭い編集などを行います。
で最後が純粋にオブジェクトで表現する層です。ドメインをオブジェクトの関係で表し、各オブジェクトが協調して動作します。これを行うために前の2層で泥臭い部分を除去します。
これら3つの層にはそれぞれインターフェイスがあり、どのような構成を取るかはその時々で考えます。
この形で、とるとカンマ区切りは外部へのインターフェイスの層にあり、この部分は必ずしもオブジェクト指向にこだわるべき場所では無いのでユーティリティクラスを使います。
javascript
1Format.separatedByComma(aaa.Value);
(ユーティリティクラスのカンマ区切りメソッドを表示の直前で呼び出し)
もちろんraccyさんの例のようにNumberを拡張するのもありだとおもいます。しかしその場合も値を保持するオブジェクトの外で編集するため、AAAやBBBに編集された値を返すメソッドは作りません。
追記ー
共通部分を外部から呼び出すのが正しいという意味ではなく、場合によって選択すべきだというのが主張です。その点が分かりにくくなっていたので追記します。
実際のところカンマ区切りというのは単なる一例でしょう。しかし、他の内容でも同様で責務として正しいのか検討すべきだと思います。
手段としては、大きく分けて、継承(・ミックスイン)・移譲・呼び出し元の三種類(四種類)あると思います。後の方ほど疎結合になります。
カンマ区切りはモデルではなく表示部の問題なので呼び出し元の表示部のクラスか責務を受け取るべきです。
(また、表示用のモデル毎に表示形式を保持させるのは個人的にはちょっとOOに偏り過ぎだと感じます。画面クラスがまとめて管理すべきだとかんがえます。)
追記ー
質問者さんは退会されたようですが、「現場で役立つシステム設計の原則」を読んだので自分用のメモとしてその点を追記します。
「3章 業務ロジックをわかりやすく整理する」で、三層+ドメインモデルについて書かれていますが、質問にある部分は三層の中のプレゼンテーション層の関心事です。明確には書いてないようですが、三層はそれぞれパッケージが別に分けるのが自然で、また、同一のドメインモデルを扱っても対象になるプレゼンテーションの対象が異なる(例えばWebUIとWebAPI)ならば、それぞれ別のプレゼンテーション用のクラスを定義します。
それに対して、実際の表示の部分は移譲を使うといいのではないでしょうか。といっても、金額の表示であれば金額用の表示をする(ヘルパー)オブジェクトに値を渡して、CSSのクラスなり編集結果なりを返すということになると思います。
投稿2017/10/01 14:02
編集2017/12/18 02:29総合スコア2883
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
退会済みユーザー
2017/10/03 02:39
2017/10/03 03:10
退会済みユーザー
2017/10/04 23:22
退会済みユーザー
2017/10/04 23:27
2017/10/05 00:37
退会済みユーザー
2017/10/06 07:57
0
例えば、addCommaという数字を3桁区切りでカンマをつける全く同じメソッドが複数間のクラスに存在する場合、
親クラスを作ってあげるとか、そのメソッドを別ファイルに保存してインポートして使うとか、いろいろと選択肢はあると思うのですが、
適切な方法の判断の仕方がわかりません。
それはクラスがおデブさんになるから嫌だなぁ…
例えば数値を3桁区切りのカンマに変換すると使い勝手が超絶悪いStringになるから、
出力の直前までNumberのまま利用して、Viewが画面表示に使う直前で初めて3桁区切りのStringになって欲しい。
そうなると、クラスではなく関数やプロトタイプ拡張に存在するのが正しい実装になる。
クラスやインスタンスは出来る事が少なければ少ない程良い。
責務を必要最低限に絞るからこそ、このクラスは何のために存在しているかがコードから一発で分かるわけで、
addCommaやらなんやらのどうでもいいメソッドがゴテゴテ付いた豪華なクラスだと存在意義が見えてこない。
さらにいうと、addComma
ってなんだろう。
カンマを加えるのに成功したのかという結果(trueとfalse)を返すメソッドなの?
どうされたのかを表すならcomma_separated
の方が良さそう。
それを踏まえてコードにしてみたよ。
JavaScript
1const vouchar = new Voucher(1050) // vouber: 領収書 2 3// case1: メソッドに追加 4console.log(voucher.commaSeparatedValue()) 5// 1,050 6 7// case2: 関数定義 8// commaSeparated :: Number -> String 9const commaSeparated = it => it.toLocalString() // 横着した 10console.log(commaSeparated(voucher.value)) 11// 1,050 12 13// case3: Number.propertyに追加 14Object.defineProperty( 15 Number.prototype, 16 'commaSeparated', 17 {get: function(){return this.toLocaleString()}} 18) 19console.log(voucher.value.commaSeparated) 20// 1,050 21
誰でも使うような普遍的な処理は、専門にしてる誰かにやってもらえばいい。
「テレビを見たくなったらリモコンを隠すアルバイト」みたいな仕事作ってその人に移譲すればいいわけだ。
投稿2017/10/01 05:59
編集2017/10/01 06:00総合スコア21158
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
退会済みユーザー
2017/10/01 06:21
退会済みユーザー
2017/10/01 07:00
退会済みユーザー
2017/10/01 07:26
退会済みユーザー
2017/10/01 07:47
2017/10/01 08:28
2017/10/01 08:32
0
共通処理がオブジェクトの状態に依存するのであれば、親クラスとして汎化することができる場合もあるかと思います。そのような場合は継承を用いるとよいのではないかと思います。
そのような特性を持たない(例示の addComma のように value の値を加工して返却するのみ、等の)場合はユーティリティクラスとして static メソッドでの実装を行ってもよいのかと思います。
ユーティリティクラスは、1つのクラスにメソッドを詰め込みがちだったり、なぜそのクラスが処理を行うのかという意味付けがしづらかったりと扱いづらい面もありますが、すべての操作をオブジェクト化することのコストとの兼ね合いもありますので、一概に不可とする必要もないと思います。
余談ですが、共通で使用する可能性のあるメソッドや定数をベースとなるクラスに定義して、作成するクラスはすべてベースクラスを継承する、というつくりを見たことがありますが、プログラミング時になぜその機能が使えるのかの理解がすぐにできず、分かりづらいと感じました。
投稿2017/10/01 04:42
退会済みユーザー
総合スコア0
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
退会済みユーザー
2017/10/01 06:23
退会済みユーザー
2017/10/01 06:33
退会済みユーザー
2017/10/01 07:25
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。