2008年9月28日
アーキテクチャ選定ではメリットとデメリットを考える
ではフレームワークやアーキテクチャの選定は合目的でなくてはならない、ということを言いましたが、
「システムの贅肉」という曖昧な概念を説明できていませんでした。
なんとなくイメージはできるんだけど、論理的な定義をうまくあたえれていない…。
骨格 = 要件の大きさ
まず、確認しておくことは、システムの規模そのものの大きさです。
要件定義・要件開発において、スマートになるように要件を決めよう、という話題ですね。
お客さんに「ちょっとここはこうなるようにしてほしい」とか言われたときに気にするのがこの点。
今の設計に無理なく乗るなら軽く「いいですよ」と返せるところですが、その一見小さく見える事象に対応するために
骨格がいびつになるような場合は悩んでしまいますね。
さて、そんな要件開発ですが、絶対値というか、要求される事象の大きさの下限は決まっていて、
目的を達成するために必要な最低限の骨格を作るだけでも、とても大きな骨格になってしまうものもあります。
ネズミかゾウかではなくて、肥満か筋肉質かだろうが言っているのは
この骨格の大きさと、プログラミングのまずさからくる贅肉を混同するな、ということですね。
体の大きさ = 実装の大きさ
要件を実装するにあたって、実装手段によってソースコード、つまるところ実装の大きさが異なります。
大雑把にいえばステップ数ですね。
(ステップ数という単位は誤差が酷いのであまり使いたくないですが、プログラムの大きさと正の相関を持つのは事実)
成長の早さ = リリースまでの期間
システムを完成させるまでの期間、というのはゾウなりネズミなりが成人するまでの期間に相当するでしょう。
ゆっくり時間をかけて成長(プログラミング)が進み、完成までに数年の歳月を費やすシステムもあれば、
非常に高速で成長しわずかに数か月で稼働にこぎつけるシステムもあることでしょう。
一般には、小さなシステムの方が、リリースまでの期間は早く、大きなシステムほどリリース時期は遅い。
養育費用 = 開発コスト
成人までの間にどれだけの費用を投入したのかという数値ですね。
成人後の成長 = アジャイル性
アジャイルなシステムというのは成人が早い。割と未熟なうちにリリースしてしまう。
リリース後の成長も大きく、成人してからも代謝が高いのですね。
対してウォーターフォール式のシステム開発などは、成人後にほとんど成長は止まってしまう。
下手をすると成長などまるでなくて、「老後の介護」にばかり手間がかかるということもありえるのです。
代謝を高めるための設計手法と言うのもあります。こうした後の変更がある場合にそれは活きてくる。
でも、いざそのときが来ない限りはそうした設計の良さというのは日の目を見ない。
ソフトウェアの保守性はアップサイドリスクである
で言ったのはそういう事情のことなのですね。
寿命 = システムの寿命
システムは改修をしつつ、使われるわけですが、やがて寿命を迎えます。死因はさまざまですが、
- 土台となるOSやミドルウェアが死んだ
- 要求の変更に対応できなくなった
というのが大きいかなと思います。
システムというのは設計時に、いろんな前提条件を加味します。こういう部分は変更が容易なように、この辺は変えなくてもよい、とかいろいろ。
「全部対応できるように」だといくらカネがあっても足りません。
それらの前提が覆ってしまったとき、それがシステムが死ぬべき時だと私は考えます。
贅肉・筋肉 = ???
さて、問題となるのがここですね。「実装の質」と言いたいところですが、ひとことに「質」と言っても質にも種類があるので混乱しやすい。
例えば、高い処理効率(パフォーマンス)をたたき出すようなカリカリにチューニングされたソースコードというのは、
変更に対してとても弱い。柔軟性に欠けるわけですね。
逆に抽象化を施し、仕様変更などにも柔軟に対応できるようなコードというのはパフォーマンスで劣る。柔軟だけど速度は遅い。
なので、これらのメリット・デメリットを考えた上で適度なところでバランスする必要があります。赤筋(反応が遅いが持続力がある)と白筋(反応が速いが持続力がない)みたいなもんですね。ハンマー投げの室伏広治氏は白筋が多く、マラソンは苦手だというエピソードもあります。状況を見て合致する「質」を選ばなくてはいけない。
ただ、一見して贅肉と分かるものも中にはあって、
- コードクローン。要するにコピー&ペーストによって冗長なコードになっている状態
- 構造化がうまくされていない
- データ構造が非効率
といったところはかなり目立つ贅肉ですね。データ構造が悪いと、それを処理するプログラムも煩雑になって贅肉が贅肉を呼ぶ状態。
こうした、明らかな贅肉はそぎ落とすことでコードが数分の一になることがありますが、経験則からして1/10を超えない。
どんだけ贅肉で太らせようとても骨格が支えられないほどには大きくなる前にシステムが破たんするのではないかと思います。
人間の体重も世界記録では700kgぐらいらしく、限界までいっても10倍そこそこのようですね。
体脂肪を落とす効率
システムの贅肉を落とし、体脂肪率を10%にするためのコストと、体脂肪率10%から1%に落とすコスト、1%から0.1%に落とすコストが概ね等しいと思います。(無責任な社会、限定責任な社会から着想)
まぁこれは概念的な話なので数字が正確かというと怪しいのだけども、指数関数的だよね、というのは理解いただきたい。
過度なダイエットは費用ばかりが嵩むので、ほどほどの贅肉は許容せざるをえないというわけです。
とはいえ、このほどほどの贅肉はコードが数分の一に縮むほどの贅肉じゃない。人間だって体脂肪率が0%になることはないでしょう?多少の脂肪に病的に神経質になっても仕方がない。
合目的なアーキテクチャ選定
フレームワークやアーキテクチャの選定は合目的でなくてはならない。そこで取り上げられる評価軸は「小規模 - 大規模」なんて単純な1軸ではないわけです。
- システムの骨格の大きさ : 要求事項の絶対値
- システムの実装の大きさ : ソースコードの量
- リリースまでの期間 : 開発速度
- リリースまでのコスト : 開発生産性
- リリース後のコスト : 可変性
- システムの寿命
「時間、品質、料金」のうちから、2つを選択できる、なんてジョークがあるのですが、示唆深いと思いませんか?
これらのうち、何を活かして何に目をつぶるのか、その比率はどの程度にするのかというのが、アーキテクチャ選定で悩む部分です。
小さなシステムは、使い捨てにすることもできる。リリースまでの期間が短くコストも小さいなら、定期的に使い捨てにするという方法論もアリでしょう。
過去のリソースを使いたいからCOBOLで開発というのもコスト・リスクが見合うならあるいはアリでしょう。
コンセプトは前提条件の上に繰り広げられるものです。コンセプトだけ聞くと「ないわー」と思うようなことでも、特定のシチュエーション下では「そうせざるを得ないね」という合理的なものであったりするのです。
目的があってその実現手段としてアーキテクチャを選ぶのですから、アーキテクチャ単体だけを取り上げて安易に良しあしと言ってはいけない。
出来ることなら、技能あるチームで今風のアーキテクチャで今風の設計をするようなシステム開発をやりたいものですね。
2008年9月25日
アルゴリズムであるとか、アーキテクチャというのは、必ずメリットとデメリットがあります。
プログラマは前提条件に合わせてメリットが活き、デメリットは表に出ないように考えアルゴリズムの選定します。アーキテクトがフレームワークやアーキテクチャを選定する時も同等です。
「大規模プロジェクトではどうするか」を考えるより、「大規模にしないためにはどうするか」を考えようというエントリに対し、私はネズミかゾウかではなくて、肥満か筋肉質かだろうと答えたわけですが、さて、では筋肉質なシステムとはどんなシステムか?という話になります。
思うに、筋肉質なシステムというのは、選定したアーキテクチャなり、フレームワークなりのメリットが活き、デメリットが隠れるようになっている、そんなシステムのことではないでしょうか。
だから、それが例えばCOBOLで新規開発という話だったとしても、「過去資産を活用する」というメリットが大きく、その過去資産の活用のための工数がデメリットにならず、過去資産を流用することで未来の保守の工数が嵩むといったデメリットもでない、というなら筋肉質と言えましょう。
通常は、このメリット・デメリットを秤にかけて、相応にメリットが大きいという状況でないといけないわけで、東京海上の例では、本当にデメリットは小さいの?と疑問に思うわけです。
さて、ではJavaのフレームワークの中ではすっかり枯れた技術という評判のStrutsはどうなのでしょうか?Strutsは初出が2001年と古く、近年のAJAXなどの対応を盛り込むにはマッチしないフレームワークだと私は評しています。非同期通信を伴わない、単純なWebシステムで工数を抑えて機能を量産するというなら、それなりにメリットは活きるでしょうし、デメリットは隠れるのではないかと思います。
これはStrutsに限らず、PHPやRubyでの開発でも、状況によってはメリットは活きず、デメリットが表出することになります。そうしたミスマッチな選択をしたプロジェクトは、その無理から脂肪のような無駄なソースが膨らみ、デメリットだらけとなってしまうことでしょう。私が選定するならFlexなどのリッチクライアントとかかなぁ。ユーザ容貌的にそちらの方がマッチして贅肉が少なくなる気がしますね。
2008年9月12日
明日は「エンジニアの未来サミット」ですね。
私も参加します。会場では名札さげるようにしますので、お気軽に声をおかけください。
そもそも何のイベントなの?
ことの始まりは10年泥で話題になったIPAX2008。情報処理推進機構(IPA)が開催したものだったのですが、
いろいろ議論を呼ぶ展開になりました。
これを受けて、Seasarで著名なひがやすを氏が
「IT業界の重鎮に期待せず、アルファギークと学生の討論会はいかが」にて
とはいえ、重鎮たちと学生を討論させても、学生がIT(SI)業界に夢を持ってくれるとは思えないので、ここで1つ提案をしたい。
小飼弾のアルファギークに逢ってきたのメンバーと学生会の討論会を開くのだ。
もちろん、司会は、ダンコーガイ。いいよね、弾さん。
と言いだしたのが始まり。本当に実現させちゃうんだからひがさんは凄いですよね。
自分はアルファギークと言うほどでもありませんが、会場をうろついているので学生の参加者の方は
気軽に質問をぶつけてみてくださいね。
2008年9月8日
Javaで一般に内部クラスと呼ばれるものはバリエーションが4つあるのですが、リフレクションでこれらを用いる場合に
結構ハマるポイントがあります。
| 判別法 | 外部クラスのインスタンス | コンストラクタ |
staticなネストしたクラス | Class#isMemberClass()がtrue かつ、Class#getModifiers()がModifier#isStatic()でtrue |
アクセス不可 |
|
エンクロージング型内部クラス | Class#isMemberClass()がtrue かつ、Class#getModifiers()がModifier#isStatic()でfalse |
外部クラス名.this でアクセス可能 |
第一引数に暗黙に外部クラスのインスタンスを受け取る |
メソッド内で定義されるローカルクラス | Class#isLocalClass() |
宣言されたメソッドがstaticなら不可。インスタンスメソッドならば、外部クラス名.this でアクセス可能 |
インスタンスメソッドで宣言された場合、第一引数に暗黙に外部クラスのインスタンスを受け取る |
匿名クラス | Class#isAnonymousClass() |
外部クラス名.this でアクセス可能 |
第一引数に暗黙に外部クラスのインスタンスを受け取る |
とあって、staticなネストしたクラスとエンクロージング型内部クラスの判別が面倒臭い。
また、staticなネストしたクラス以外は、暗黙にコンストラクタの第一引数に外部クラスのインスタンスを受け取るので
リフレクションでConstructorを扱う時にはシグネチャに注意。
とくに、メソッドで定義するローカルクラスは、宣言がstaticメソッドか、インスタンスメソッドかで
違ってくるためClass#getModifiers()の値を確認して丁寧に対応する必要があります。
2008年9月1日
わんくま勉強会の地方開催シリーズ。北陸のエンジニアのみなさん、お待たせしました。
2008年11月8日に「11/8 わんくま富山勉強会 #1」を開催いたします。
副題として「Javaの未来を考える」とテーマを設けさせてもらいました。
普段仕事でJavaを使っている方々をメインターゲットに、
今後の業界動向を展望するセッションで出迎えたいと考えています。
会場は富山県民会館です。
JR富山駅から徒歩10分ほどですので、近県の方もぜひお越しください。
大きな地図で見る
セッション内容などについては、詳細が決まり次第お伝えしますのでお楽しみに!
2008年8月20日
前回はまず、ジェネリクス型パラメータを伴うList同士の代入互換性について述べました。
今回はそれらのListのadd()メソッドとget()メソッドについて見てきたいと思います。
なお、前回同様に C extends B,
B extends A
という継承関係があることとして以下話を進めます。
入力値の制約
前回で<? extends B>型には
<B>も
<C>も
<? extends C>も代入できると述べました。
List<? extends B> listBEx = new ArrayList<C>();
ということができるわけですね。
さて、このlistBExにadd()をしてみるとしましょう。
listBEx.add(new B());
実は、これがコンパイルエラーになるのです。
List<? extends B>型には
B型をadd()できないのです!
というのも、さきほどlistBExはArrayList<C>型で
初期化しましたね。もし、Bをadd()できるとしたら、
ArrayList<C>型に
B型がadd()されてしまうことになります。これでは矛盾してしまいますね。
ですから、List<? extends B>では
型の安全性が破壊されないように、add()できるのは
List<? extends B>に代入可能な
List全てにadd()可能なものだけしかadd()できないように制約が掛けられます。
<? extends B>の範囲と
ArrayList<C>の範囲、
そしてBオブジェクトの位置を確認してみてください。
Bオブジェクトを型安全にadd()することができないのが分かるでしょうか?
では<? extends B>に対して何がadd()できるのでしょうか?
listBEx.add(null);
だけが可能なのです。使えませんね…。
さて、add()メソッドはこのような制約があるわけですが、get()メソッドなどは普通に使えます。
この違いは何なのでしょうか?
これは、メソッドの引数にジェネリクス型パラメータが含まれる場合に発生する制約です。
引数にジェネリクス型が含まれるadd()などではこのような制約が発生し、
引数にジェネリクス型を含まないget()などでは制約は発生しません。
オブジェクトに対しての入力値がジェネリクスの代入互換性に矛盾しないようにするために
存在する制約というわけなのです。
出力値の制約
List<? extends B>からの
get()は問題なく行え、B型の変数に受け取ることができます。
B b = listBEx.get(0);
これは、<? extends B>に
<B>が代入されていようが
<C>が代入されていようが、
get()で取り出されるオブジェクトは「B型を継承した何か」ですから、B型に安全にキャストできるわけです。
ここで、List<? super B>を考えてみましょう。
<? super B>には
<B>や
<A>を代入することができます。
ということは、get()で取り出されるオブジェクトは、B型よりも上位のオブジェクト型である可能性があるわけです。
このことから、<? super B>とした場合は
全ての型のトップに位置するObject型でしかget()したオブジェクトを受け取ることができません。
Object o = listBSu.get(0);
これは前回の図を見ると分かることでしょう。再掲します。
入力値の制約 再訪
List<? super B>へのadd()はどうでしょうか?
図を見て分かるように、<? super B>に
代入可能な<B>や
<A>は、すべてBオブジェクトを
受け入れることができます。
そのため、ジェネリクスが<? super B>であれば
B型をadd()することができます。
2008年8月18日
Javaのジェネリクスはかなり強力で、相当の型を表現できるのですが、
代償として非常に複雑なものとなっています。
ややこしいのは、オブジェクト指向の部分の型の代入互換性と、
ジェネリクス型パラメータの部分の代入互換性は、表現こそ似ているものの、
その意味するところはまるで違うと言うことにあります。
端的には、C extends B,
B extends Aの関係があるとして、
型B にはサブクラスであるCをキャストなしに安全に代入することができます。
B b = new C();
しかし、ジェネリクス型パラメータの場合の
List<B> listB = new ArrayList<C>();
はコンパイルエラーとなります。
List<? extends B> listBEx = new ArrayList<C>();
であれば代入が可能です。
このように、同じ継承階層の型を扱うのにもかかわらず、その代入互換性が違うのですから
混乱するのはやむなしと言えましょう。
ジェネリクス型パラメータの代入互換性
ジェネリクスでは、単に型をBと表現した場合、Bの階層だけが対象となります。
図のBの階層だけが対象になります。
ですから、代入できるのは<B>型だけです。
List<B> listB = new ArrayList<B>();
次に、<? extends B>と表現した場合、BとそのサブクラスであるCが含まれます。
<? extends B>には<B>も<C>も代入することができます。
また、<? extends C>も代入することができます。
List<? extends B> listBEx;
listBEx = new ArrayList<B>();
listBEx = new ArrayList<C>();
List<? extends C> listCEx = new ArrayList<C>();
listBEx = listCEx;
また、ジェネリクスでは継承階層をスーパークラス側に遡る、<? super B>という記述もできます。
この場合は、<? extends B>とは逆方向の範囲をカバーします。
この<? super B>には<B>や<A>や<? super A>を代入することができます。
List<? super B> listBSu;
listBSu = new ArrayList<B>();
listBSu = new ArrayList<A>();
List<? super A> listASu = new ArrayList<A>();
listBSu = listASu;
このように、<>の内側と外側では異なる代入規則があることをまずは明確に意識してください。
2008年7月15日
ゆの in Javaとかに嵌って放出が遅れましたが、ソート祭りの話題。
非破壊で順序を変更してとりだす
GoFデザインパターンのIteratorパターンを用いて元になるListには手を加えずに
ソートと同じような効果を得るサンプルです。
public class SortIterator<T> implements Iterator<T>, Iterable<T> {
/** ソート対象 */
private Iterable<T> target;
/** ソート用Comparator.この実装を変えることで並ばせ方を変えられる */
private Comparator<T> comparator;
private T preElement;
private List<T> equalValues;
/**
* コンストラクタ
* @param target ソート対象。SetやListなどを渡すことができる
* @param comparator 並ばせ方を決めるComparatorの実装
*/
public SortIterator(Iterable<T> target, Comparator<T> comparator) {
this.target = target;
this.comparator = comparator;
this.equalValues = new ArrayList<T>();
}
@Override
public boolean hasNext() {
T min = null;
targetLoop:
for (T elem : this.target) {
// 前回までに返した値より小さいものは飛ばす
if (this.preElement != null &&
this.comparator.compare(this.preElement, elem) < 0) {
continue;
}
// 前回と同じ値のものは、重複を確認してすでに返した値なら飛ばす
if (this.preElement != null &&
this.comparator.compare(this.preElement, elem) == 0) {
// 参照の同値性で判断するために敢えてcontains()は使わない
// contains()だとequals()による比較になるため
for (T value : this.equalValues) {
if (value == elem) {
continue targetLoop;
}
}
this.preElement = elem;
this.equalValues.add(elem);
return true;
}
// 前回までに返した値より大きいもののうち、最小のものを探す
if (min == null) {
min = elem;
continue;
}
if (this.comparator.compare(min, elem) < 0) {
min = elem;
}
}
if (min == null) {
return false;
}
this.equalValues.clear();
this.equalValues.add(min);
this.preElement = min;
return true;
}
@Override
public T next() {
return this.preElement;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
@Override
public Iterator<T> iterator() {
return this;
}
}
これを以下のように呼び出します。
List<Integer> list = new ArrayList<Integer>();
list.add(new Integer(3));
list.add(new Integer(1));
list.add(new Integer(4));
list.add(new Integer(2));
list.add(new Integer(1));
SortIterator<Integer> ite = new SortIterator<Integer>(list,
new Comparator<Integer>(){
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
for (int i : ite) {
System.out.println(i);
}
出力結果は
1
1
2
3
4
ちゃんと整列していますね。
解説
このクラスではソート対象のIterableと比較に用いるComparatorを保持して
hasNext()のたびに今までに返した値より大きいもののうち一番小さいもの、
つまり「次に小さい奴」を探してnext()でそのオブジェクトを返します。
最初に「データをちょうだい」と言われた時に「一番小さい奴」を探して返し、
「次のデータをちょうだい」と言われたら「次に小さい奴」を探して返し、
…。
でも、それで利用側から見ると並び変わったように見えるんですね。
設計に利用するヒント
Listの中身そのものを並び替えた方が効果的なシチュエーションの方が多いかもしれませんが、
一時的に他の並び方を使いたいという場合はIterator側に並び替えを仕込むのも一つの手です。
これは非破壊であることがメリットで、複数のスレッドからオブジェクトの持ついろんな属性での
並び替えを並列に取得することもできます(その間に元のListが変更されると動きがおかしくなりますが…)。
そういう意味ではImmutableパターン、つまりオブジェクトを変更不可能にすることで
並列時の安全性を確保するようなケースで有用です。
データを利用する人が多岐にわたる場合、自分の都合だけで並び替えるわけにいかないかもしれません。
そのような場合に、外付けでデータの順番が変わったように見せる手法があることを知っておくと
うまい設計を起こすヒントになるかもしれません。
外から利用する分には中のデータの表現方法なんて隠蔽されていて関心の外ですから、
必要になった時に自転車操業的に「次に小さい奴」を探して渡す、でも大丈夫なんですよね。
2008年7月13日
ゆの in Javaの新作。
public class ひだまりスケッチ {
static class x365 extends RuntimeException {
public x365(String message) {
super(message);
}
}
public static void main(String[] args) {
int _,X=1;
X = _ = X;
try {assert
X / _ / X < _:"来週も見てくださいね!";
} catch (Throwable t) {
throw new x365(t.getMessage());
}
}
}
実行すると以下のような感じに。
動かす場合の注意点
assertを使っているので起動時に-eaをつけて以下のように起動する必要があります。
java -ea ひだまりスケッチ
解説
基本的には
矢野さんのゆの in Javaの改変版です。
paulowniaさんの
StackTraceを使った技法を見ていて、もうSystem.out.printlnしなくていいやと割り切りました。
2008年7月11日
前回のが、
肝心な部分はUnicodeエスケープを利用して実は全部コメントになっているというインチキ甚だしかったので、
真面目に取り組んでみました。
package ひだまりスケッチ;
public class x365 {
static class 来週も見てくださいね extends X {}
static 来週も見てくださいね X = new 来週も見てくださいね();
static class X {
public <T> boolean 宿題だよ(T t) {
System.out.println(t.getClass().getCanonicalName());
return true;
}
}
// ----- main method -----
public static void main(String[] args) {
boolean _ = true;
_ = X instanceof
X|_|X .< 来週も見てくださいね
>宿題だよ(X);
}
}
コンセプト
Javaの場合、演算子オーバーロードができないので、アスキーアート(以下AA)部分をどう処理するかというのが
大きな問題となります。「X / _ / X」の部分と吹き出しの「<」ですね。
今回はちょっとAAをいじってて「X | _ | X」になっており、
目の部分が"/"(除算)の代わりに"|"(論理演算のOR)になっています。
今回は、セリフの吹き出しのための "<"を不等号ではなく、
ジェネリックなメソッドの型パラメータ指定のための<>として使っています。
セリフの中身「来週も見てくださいね」が文字列ではなくてクラス名になっているんですね。
ここで厄介なのはふたつのXです。AAの左側と右側でXが出てきますが、
右側のXは続くメソッド呼び出しのためにオブジェクトの参照である必要があります。
しかし、左側はOR演算子が隣接するのでbooleanかint型である必要があります。
そこで、Xという型名のクラスを用意して、「クラス名としてのX」と「変数としてのX」と
ふたつのXを使い分けています。型名をOR演算子に隣接させるためにinstanceof演算子を利用しました。
メッセージはクラスXの「宿題だよ」メソッドで出力していますが、
Class#getCanonicalName()を使ってクラスの正規名を出力しています。
こうすることで、パッケージ名+親クラス名+クラス名となって出力されるわけです。
間にピリオドが入っちゃうのは御愛嬌と言うことで。
コンセプト比較
矢野さんの手法は、
assertキーワードを使って"<"によって作られたbooleanを利用して
assert警告のメッセージに吹き出しの文字列を利用することで演算子を処理しています。
Thread#setDefaultUncaughtExceptionHandler()でassertを拾ってメッセージ表示というのが
キモいところです(褒め言葉)。
Derive Your Dreamsさんの手法は
AA部分はintによる演算にしているのですが、Xや_の変数を内部クラスのフィールドにし、
static importを利用してクラスを次々とロードし、satatic フィールドの初期化を起こさせ、
その初期化処理でメッセージを出力するという機構。
ふたつのX問題は片方をstatic import、
片方を親クラス+子クラスの形で呼び出すことで解決しています。
注意
このコードはパッケージ名を解決してやらないと動かせません。
"ひだまりスケッチ"packageなので、ソースフォルダの下に"ひだまりスケッチ"という
フォルダを作ってx365.javaを作成する必要があります。出力結果は
ひだまりスケッチ.x365.来週も見てくださいね
となります。