デザインパターンがわからないので再入門してみる
デザインパターンがわからない理由
- そもそもデザインパターンってなに?
- なんとなく一読したけど、頭にはいってこない
- 委譲という考え方がしっくりこない
- サンプルコードは理解できるが、じゃあいざ実践コーディングするときにデザインパターン使えない
デザインパターンに対する理解不足と経験不足
実践経験をつめる練習問題的なものがあれば。
ただこうしたいと思ったときは、自分でコード工夫しようと思うより、ライブラリ探すこともおおいから、そもそもデザインパターンを実践しなくてもよいのかも。
また、一気にパターンを覚えようとするのではなく、
- 手っ取り早く実践に一番活用しやすいBuilder
- 一番応用がききそうなStrategyとState
の習得が最初に目指し、他のパターンはそれに慣れてからでも良いと思う。
思考の流れ
- 似たようなコードがありリファクタリング時もしくは、何回目か同じコードを書いている際にパターンを適用したい
- 最初からパターン導入が見えている場合
まずは特定のパターンだけマスターして、「最初からパターン導入が見えている場合」を実践し、デザインパターンに慣れてきたら「似たようなコードがありリファクタリング時もしくは、何回目か同じコードを書いている際にパターンを適用したい」にすれば良いと思う。
Stateパターン考察
キーポイントは「状態」と「共通処理」
複数の状態によって処理が変わる。
コードサンプル
class NormalMode {
constructor(character){
this.character = character;
}
attack () {
return 15;
}
defense(){
return 15;
}
}
class BerserkerMode {
constructor(character){
this.character = character;
}
attack(){
return 20;
}
defense(){
return 10;
}
}
class PoisonMode {
constructor(character){
this.character = character;
}
attack(){
return 10;
}
defense(){
return 10;
}
}
class Character {
constructor() {
this.modes = {
normal: new NormalMode(),
berserker: new BerserkerMode(),
poison: new PoisonMode()
};
this.currentMode = null;
this.changeMode('normal');
}
changeMode(mode) {
this.currentMode = this.modes[mode];
}
attack(){
this.currentMode.attack();
}
defense() {
this.currentMode.defense();
}
}
const character = new Character();
character.attack();
character.changeMode('poison');
character.attack();
Stateパターン応用
バーサーカーモードやポイズンモードになったあと一定時間たつと通常モードにもどる。
activateメソッドないに setTimeoutで状態を変化させるチェックを入れるか、もしくはイベントハンドラーによってあるイベントが起きると状態を変化させたりする。
class NormalMode {
constructor(character){
this.character = character;
}
attack () {
return 15;
}
defense(){
return 15;
}
activate(){
// do nothing
}
}
class BerserkerMode {
constructor(character){
this.character = character;
}
attack(){
return 20;
}
defense(){
return 10;
}
activate(){
setTimeout(() => {
this.character.changeMode('normal');
}, 30000)
}
}
class PoisonMode {
constructor(character){
this.character = character;
}
attack(){
return 10;
}
defense(){
return 10;
}
activate(){
setTimeout(() => {
this.character.changeMode('normal');
}, 30000)
}
}
class Character {
constructor() {
this.modes = {
normal: new NormalMode(),
berserker: new BerserkerMode(),
poison: new PoisonMode()
};
this.currentMode = null;
this.changeMode('normal');
this.currentMode.activate();
}
changeMode(mode) {
this.currentMode = this.modes[mode];
this.currentMode.activate();
}
attach(){
this.currentMode.attack();
}
defense() {
this.currentMode.defense();
}
}
const character = new Character();
character.attack();
character.changeMode('poison');
character.attack();
Stateパターンを適用する場面
システムに持ち込むとわからなかったけど、Stateパターンって日常に溢れてる気がする。
例えば
- 仕事
- 家事
などいわゆるルーチンとなっているものと、その日の体調やモチベーションでその結果は変わる。もしかしたらやらないタスクもあるかも。
こういった、ルーチン化と状態という部分にStateパターンが適用できる。
結論
「Stateパターンはどんな状態でもやらなければいけない処理があり、かつ状態によってその処理方法が変わる場合に役に立つ」
逆にどんな状態でもやらなければいけない処理が殆ど無い場合は、純粋にそのタスクのメソッドにif文で状態チェックいれて異なる状態であれば処理しないもしくはエラーをだすだけでいいと思う。
someMethod() {
if(this.state !== 'HUNGRY'){
return;
}
// 何らかの処理
}
Stateパターンが習得しにくいのは、割と「どんな状態でもやらなければいけない処理」というのがシステムの中にあまり出てこないからかもしれない。
StrategyパターンとTemplateパターン
この2つは同時に覚えたほうがよい。
例として、絶対評価の先生と相対評価の先生がいたとする。
絶対評価の先生はある合計点数以上とれば良い生徒と評価するが、その合計点数を下回れば悪い生徒となる。
相対評価の先生はクラスの平均合計点以上であれば良い生徒と評価するが、その平均合計点を下回れば悪い生徒となる。
Strategyパターンで実装。
const absoluteStrategy = {
evaluate(student){
const atLeastSum = 100;
const studentSumPoint = student.math + student.japanese + student.history;
if(studentSumPoint > atLeastSum){
return 'good student!'
} else {
return 'bad student!'
}
}
}
const relativeStrategy = {
evaluate(student){
const classSumAverage = 180;
const studentSumPoint = student.math + student.japanese + student.history;
if(studentSumPoint > classSumAverage){
return 'good student!';
} else {
return 'bad student!';
}
}
}
class Teacher {
constructor(evaluationStrategy){
this.evaluationStrategy = evaluationStrategy
}
evaluate(studentId){
return this.evaluationStrategy.evaluate(studentId);
}
}
const student = {
math: 60,
japanese: 80,
history: 20
};
const absoluteTeacher = new Teacher(absoluteStrategy);
console.log(absoluteTeacher.evaluate(student));
const relativeTeacher = new Teacher(relativeStrategy);
console.log(relativeTeacher.evaluate(student));
Templateパターンで実装
class TeacherTemplate {
evaluate() {
throw new Error('evaluate must be implemented!');
}
}
class AbsoluteTeacher extends TeacherTemplate {
evaluate (student) {
const atLeastSum = 100;
const studentSumPoint = student.math + student.japanese + student.history;
if(studentSumPoint > atLeastSum){
return 'good student!'
} else {
return 'bad student!'
}
}
}
class RelativeTeacher extends TeacherTemplate {
evaluate (student) {
const classSumAverage = 180;
const studentSumPoint = student.math + student.japanese + student.history;
if(studentSumPoint > classSumAverage){
return 'good student!';
} else {
return 'bad student!';
}
}
}
const student = {
math: 60,
japanese: 80,
history: 20
};
const absoluteTeacher = new AbsoluteTeacher();
console.log(absoluteTeacher.evaluate(student));
const relativeTeacher = new RelativeTeacher();
console.log(relativeTeacher.evaluate(student));
StrategyパターンとTemplateパターンの違い
// strategy
const absoluteTeacher = new Teacher(absoluteStrategy);
console.log(absoluteTeacher.evaluate(student));
const relativeTeacher = new Teacher(relativeStrategy);
console.log(relativeTeacher.evaluate(student));
// template
const absoluteTeacher = new AbsoluteTeacher();
console.log(absoluteTeacher.evaluate(student));
const relativeTeacher = new RelativeTeacher();
console.log(relativeTeacher.evaluate(student));
大きくは委譲と継承の違い。Strategyパターンのほうが、その部分のみを部品化しているので「動的な」変更がきく。Templateパターンはあらかじめアルゴリズムにおけるバリエーションが決まっているときにこのどれか使ってねというふうに提示できる。例えば利用者などが内部実装(strategyパターンを利用しているなとか)を知る必要なく、とりあえずこのクラスを使えばよいとわかる。
Strategyパターンのほうがプラグイン的性質。Templateパターンのほうがパッケージ化されたライブラリ的性質。
継承と委譲
Object指向を勉強すると継承はわりとすんなり理解できるが委譲は理解しにくい。
量産型マイクと量産型たけしの例で次のコードをそれぞれ継承と委譲でリファクタリングする。
もともとのコード
class Mike {
saySomething() {
console.log('Hello!');
}
sleep() {
console.log('zzz');
}
}
class Takeshi {
saySomething() {
console.log('こんにちは!');
}
sleep() {
console.log('zzz');
}
}
継承でリファクタリング
継承のイメージ 「ベースの共通クラスからの進化」「粘土継ぎ足して特徴をかえる」
継承はとりあえずベースクラス(テンプレートともいう)作ってそれを拡張していく感じ。サンプルを検索すると、よくAnimalとかHumanとか出てくるのもそのイメージが強いからだと思う。よくアニメとか漫画で動物を擬人化するの流行ってるけどそれも人間をベースに継承した感じ。
class Human {
saySomething () {
console.log('hoge');
}
sleep(){
console.log('zzz');
}
}
class Mike extends Human {
saySomething() {
console.log('Hello!');
}
}
class Takeshi extends Human {
saySomething() {
console.log('こんにちは!');
}
}
const mike = new Mike();
mike.saySomething(); // Hello!
mike.sleep(); // zzz
const takeshi = new Takeshi();
takeshi.saySomething(); // こんにちは!
takeshi.sleep(); // zzz
委譲でリファクタリング
委譲のイメージ 「間に一層設けて、あとは小人さんにやってもらう」「顔はめパネル」
継承はBaseクラスを土台になり、それを継承するカスタムクラスが前面にでてきていた。委譲の場合は逆。Baseクラスが前面にでてきて、その機能を実行するものが裏側で代わりに処理をしている。Baesクラスにコンストラクタという入り口があり、そこから小人さんが入り、中から操作してくれる感じ。
class Human {
constructor(humanType){
this.humanType = humanType;
}
saySomething(){
this.humanType.saySomething();
}
sleep() {
console.log('zzz');
}
}
const mikeType = {
saySomething: () => {console.log('こんにちは!')},
}
const takeshiType = {
saySomething: () => {console.log('こんにちは!')}
}
const mike = new Human(mikeType);
mike.saySomething(); // Hello!
mike.sleep(); // zzz
const takeshi = new Human(takeshiType);
takeshi.saySomething(); // こんにちは!
takeshi.sleep(); // zzz
Factory
クラスをインスタンス化する際に
- 引数が多い
- この引数があるときは、この処理をする必要がある
- クラスにいくつかバリエーションがあり、インスタンス化処理のロジックを隠したい
というときに役に立つ。つまりは初期化処理が複雑なときに役に立つ。
ここまでが、通常のオブジェクト指向のプログラミングでの説明。
jsはとくに関数が第一級オブジェクトなのでstringやnumberのように、引数にいれたり(コールバックで活用)、返したりできる。そこで、関数自体のファクトリも作れる。
- 関数が複数の引数の組み合わせ条件によって処理内容がかわる
ときにも使える。
Objectのファクトリ
状況: クラスにバリデーションがあり、インスタンス生成ロジックをカプセル化したい
ゲームっぽいサンプルコード。ここでキャラクター生成部分が、ちょっと複雑になってきた。
// 何らかの処理
// ここでキャラクタークラスをインスタンス化
const character = new Character();
character.attack();
// 続く処理
typeという変数によって生成するキャラクターを変更したいという要求がでてきた。
クラスを直接newせずに間に一層設ける。
const createCharacter = (type) => {
if(type === 'normal'){
return new NormalCaracter();
} else if (type === 'wizard') {
return new WizardCaracter();
}
}
// 何らかの処理
// ここでキャラクタークラスをインスタンス化
const character = createCharacter('normal');
character.attack();
// 続く処理
これで、character
自体がnew
演算子に依存しなくなった。仮にここの初期化処理のロジックを変更したい場合はcreateCharacterを変更すればよい。
状況: クラスを生成するときのコンストラクタの初期化処理が複雑になってきた。また状況によって初期化に必要な値がことなる。
EntityとRepositoryパターンを導入しているとEntityの初期化ロジックが複雑になったりする。そういうときは初期化専門のBuilderクラスを作る。
例えば
class Character {
constructor(args){
this.id = args.id // オプション;
// オプション
if(args.name){
this.name =args.name.toLowerCase();
}
// 必須
this.baseHP = args.baseHP;
this.baseATK = args.baseATK;
this.baseDEF = args.baseDEF;
}
}
みたいに、必須パラメータとオプションパラメータが混じっていたり、名前は必ず小文字にする必要があるみたいな初期化ルールが複数あるとする。
Builderクラスを作る。
class CharacterBuilder {
constructor(args) {
this.character = new Character(args);
}
setName(name){
this.character.name = name;
}
setId(id){
this.character.id = id;
}
build () {
// もし必須な値が存在するのであればここでnullチェックをいれる。
if(!(this.character.baseHP && this.character.baseATK && this.character.baseDEF)){
throw new Error('必須のプロパティがundefinedです。');
}
return this.character;
}
}
Characterクラスは純粋に初期化させる
class Character {
constructor(args){
this.id = args.id;
this.name =args.name;
this.baseHP = args.baseHP;
this.baseATK = args.baseATK;
this.baseDEF = args.baseDEF;
}
}
インスタンス生成
const builder = new CharacterBuilder({
baseHP: 10,
baseATK: 10,
baseDEF: 10
});
builder.setName('Haru');
bulder.setId(10);
const character = builder.build();
関数のファクトリ
メッセージ送信処理。typeによって送信する内容が変わる。
意味のないサンプルだけど、例えばtypeにlocalと設定されていればローカルストレージに保存し、serverと設定されていればサーバーに送信するとする。同期処理と非同期処理が混じって気持ち悪けどサンプルなので許して。
const createSendMessage = (type) => (message) => {
if(type === 'local'){
localStrage.setItem('message', message);
} else if (type === 'server){
axios.post('https://example.com/messages', message);
}
}
const sendMessage = createSendMessage('local');
sendMessage({content: 'text'});