@shirakana

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

C#におけるメソッドの設計について

C#の初学者でオセロを作っています。
以下はMainFormにおけるオセロの盤上や周りのプレイヤー・石数表記ラベルなどを再描画するプログラムです。

講師がいるのですが例1に対して
「ここに戻り値・引数の存在しないメソッドがあるということは抽象度が揃っていないということだから上手くメソッドが組めていないはずだ」
「引数と戻り値どちらかはあるように組んでみるべき、そんなメソッドはロジックがない手続き型のプログラムでしかない」
というような旨の指摘を受けました。

自分としては例2のようにならないよう、また個別利用・変更など管理しやすいように1つの処理ごとにメソッドとしてまとめているつもりです。

1つのメソッドは1つの処理だけ行うべきという基準も学んでいるのですが、それらを踏まえて質問です。

・そもそもこういった設計が悪いとすれば改善はどのようにするべきなのか
・メソッドの中にメソッドがあると役割を分けられていないに等しいのか

講師としては直接の改善案はまだ伝えたくないようですが、自分だけでは全く理解が進まなかったのでここで質問してみます。

例1)

private void RefreshView(List<CellInfo> cells)
{
    DrawLabelsText(cells);
    DrawPlayerLabel();
    DrawStoneCount();
}

private void DrawPlayerLabel()
{
    Label turnPlayerLabel = TurnLabelPanel.Controls.OfType<Label>().FirstOrDefault(label => label.Name == "TurnPlayerLabel");
    Label turnColorLabel = TurnLabelPanel.Controls.OfType<Label>().FirstOrDefault(label => label.Name == "TurnColorLabel");

    turnPlayerLabel.Text = m_Board.CurrentPlayerText;
    turnColorLabel.Text = m_Board.CurrentColorText;
    turnColorLabel.ForeColor = m_Board.CurrentColor;
}

private void DrawStoneCount()
{
    Label blackStoneLabel = CountBlackStonePanel.Controls.OfType<Label>().FirstOrDefault(label => label.Name == "CountBlackStoneLabel");
    Label whiteStoneLabel = CountWhiteStonePanel.Controls.OfType<Label>().FirstOrDefault(label => label.Name == "CountWhiteStoneLabel");

    blackStoneLabel.Text = m_Board.BlackStone.ToString();
    whiteStoneLabel.Text = m_Board.WhiteStone.ToString();
}

例2)

private void RefreshView(List<CellInfo> cells)
{
    DrawLabelsText(cells);
    
    Label turnPlayerLabel = TurnLabelPanel.Controls.OfType<Label>().FirstOrDefault(label => label.Name == "TurnPlayerLabel");
    Label turnColorLabel = TurnLabelPanel.Controls.OfType<Label>().FirstOrDefault(label => label.Name == "TurnColorLabel");

    turnPlayerLabel.Text = m_Board.CurrentPlayerText;
    turnColorLabel.Text = m_Board.CurrentColorText;
    turnColorLabel.ForeColor = m_Board.CurrentColor;

    Label blackStoneLabel = CountBlackStonePanel.Controls.OfType<Label>().FirstOrDefault(label => label.Name == "CountBlackStoneLabel");
    Label whiteStoneLabel = CountWhiteStonePanel.Controls.OfType<Label>().FirstOrDefault(label => label.Name == "CountWhiteStoneLabel");

    blackStoneLabel.Text = m_Board.BlackStone.ToString();
    whiteStoneLabel.Text = m_Board.WhiteStone.ToString();
}
0 likes

8Answer

DrawPlayerLabel()TurnLabelPanelに依存しているので

private void DrawPlayerLabel(Label turnPlayerLabel, Label turnColorLabel)
みたいにすれば依存関係が解消されると言いたいのでは無いでしょうか?

個人的には TurnLabelPanel側に設定メソッドを追加して
setTurnPlayerLabel(m_Board.CurrentPlayerText)
みたいにするのが好みです

Hello, Worldにクリーンアーキテクチャを適用する人は居ないように
どの設計が良いなどは言語や規模などにもよって変わってくるので
そういう考え方もある程度に思っておけば良いと思います

1Like

Comments

  1. @shirakana

    Questioner

    回答ありがとうございます。

    Panelに直接値指定を組み込むこともできるんですね…

    やはり好みの問題なのかなぁと思っています

    仕事なので目線が合わないときっと困るんですが、よくあることなんですかね…

その指摘は正しくないと思います。

戻り値・引数の存在しないメソッドがあるということは抽象度が揃っていない

戻り値・引数の有無と抽象度の高低は関係ありません。例1では UI の更新をコンポーネントの種類別に切り分けてあり、抽象度は揃っています。

引数と戻り値どちらかはあるように組んでみるべき

どちらもない場合も普通にあります。たとえば、フィールドやクラス外のデータ(シングルトンオブジェクト、ファイル、ネットワークなど)を読み書きし、エラーは例外のスローで表すメソッドは引数も戻り値も持ちません。

ただし、引数と戻り値以外でデータをやり取りすると読みづらいコードになりやすく、テストも書きづらくなりがちです。オブジェクトがその寿命の間じゅう持っているのが自然なデータはフィールドで持ち、それ以外は引数で渡すというような意識は必要だと思います。

そんなメソッドはロジックがない手続き型のプログラムでしかない

主張がよく分かりません。何らかの処理をしていればロジックはあります。また後半について、オブジェクト指向↔︎手続き型の対比であれば、引数も戻り値もないメソッドはオブジェクト指向では十分にあり得るため、手続き型でしかないというのは違和感があります。関数型↔︎(オブジェクト指向を含む広義の)手続き型の対比であれば、確かに副作用だけがあるメソッドは関数型的な書き方ではないといえますが、オブジェクト指向が強い C# で関数型を目指す意味は薄いです。

以上を踏まえて、

・そもそもこういった設計が悪いとすれば改善はどのようにするべきなのか

規模感にもよりますが悪い設計ではないと思います。

・メソッドの中にメソッドがあると役割を分けられていないに等しいのか

そんなことはありません。

1Like

Comments

  1. @shirakana

    Questioner

    回答ありがとうございます。

    現状は自分でかみ砕いた視点でしかないので講師が伝えたいことが厳にこれであるかはわからないのですが、自分の中に限れば回答いただいた通りの認識で書けていたので自信の助けになりました。

組織のコーディングルールがあって講師はそれに従うように言っているのでしょうか? それとも、講師の個人的なポリシーに基づいての指摘でしょうか? どっちにせよ、そういう話だとすれば、ここで聞いても講師の望む答えを得るのは難しいと思います。

私の個人的な好みは聞きたくないとは思いますが、あえて言わせてもらえると、例 2 が良いと思います。理由は、メソッドに分ける意味が無さそうだからです。

1Like

Comments

  1. @shirakana

    Questioner

    回答ありがとうございます。

    組織のルールもありつつ、それを踏まえた講師のポリシーもありつつではあると思います。

    今後自分もそれに沿っていかなければならないからこそ悩んでいますが、やはり講師の詳細な答え待ちにしかならないですよね…

    メソッド化についてはパスの処理中などでプレイヤーラベルのみ再描画する場合があるのですが、置いたコマを描画する(DrawLabelsText)などの余計な処理を念のため再度挟まないように分けている面があり、そこに再利用するためにもメソッド化しています。

  2. 講師の詳細な答え待ちにしかならないですよね…

    講師の指導を受けているなら講師に聞くのが当たり前かと

    そこに再利用するためにもメソッド化しています。

    DrawPlayerLabel や DrawStoneCount を RefreshView 以外の別の場所でも使う(再利用する)ということであれば、最初の質問にその旨書きましょう。書いてないことは他人には分かりません。また、例 2 のようなコードが示してあるということは再利用することはないと読めます。

「引数と戻り値どちらかはあるように組んでみるべき、そんなメソッドはロジックがない手続き型のプログラムでしかない」

SoundPlayer.Play、IDisposable.Disposeのように、単純な処理を実行して結果を知る必要が無いメソッドなら、引数・戻り値が無い事は標準ライブラリでも普通に有ります。引数・戻り値が無いメソッドを作ってはならないというルールは、言語上には存在しません。
特定の仕様・設計思想に従って作って欲しいという事であれば、講師は最初からそれを具体的に説明するべきでしょう。

1Like

「そもそもこういった設計が悪いとすれば、改善はどのようにするべきか?」

→ 今の設計(例1)は、役割ごとにメソッドを分けていて十分良い構成だと思います。
ただし、「学習フェーズ」として改善するなら「引数を使って必要な情報を渡す」ことで、処理の流れがより明確になり、再利用やテストもしやすくなるということではないでしょうか?。
「どこから何を取得して表示するか」がブラックボックスになりすぎると、可読性や保守性が下がるため、データの流れを引数で明示するのもよい設計だと思います。
もちろん「引数や戻り値がないのが悪」なのではなく、「設計が曖昧になりやすいので注意が必要」ということです。

「メソッドの中にメソッドがあると、役割を分けられていないに等しいのか?」

→ 必ずしもそうではありません。
上位のメソッドが全体の流れを担当し、下位のメソッドが具体的な処理を担当するのは、役割分担として自然で、むしろ分かりやすい書き方です。

「引数を使って依存を明示化する」「メソッドの独立性を高める」など、将来の拡張性やテスト性に向けたステップアップの観点を講師の方は求めているのかもしれませんね。

1Like

Comments

  1. @shirakana

    Questioner

    回答ありがとうございます。

    現状の理解ですと、引数は複数の可能性があるからこそ設定されるものであって必ず使用される形が決まっているのであれば直接記載しておけばよいと考えていました。

    回答いただいたことで「メソッドが成り立つために必要な情報」を引数として設定することで可読性が上がる(と解釈しました)ということを初めてイメージできたのですが、わざわざ固定のものを宣言するのは無駄な書式ではないのか?と思って直接指定してしまっていました。

    不変であろうとしても引数を宣言するべきなのでしょうか?

  2. 不変であろうとしても引数を宣言するべきなのでしょうか?

    いいえ。今回のような仕様で使わないのに無理に引数を追加する必要はないです。
    「わざわざ固定のものを宣言するのは無駄な書式ではないのか?と思って直接指定した」は正解だと思います。

    あくまで「学習フェーズ」だということを前提に・・・

    例えば規模の大きなプロジェクトやチーム開発では、「このメソッドが何に依存しているか」を明示することがとても重要になることも多いです。

    引数があることで、「この関数は何をもとに動作しているのか?」がメソッドの宣言だけで判断できるため、他の人がコードを読んだり、テストを書いたり、差し替えの実装を行うときにとても助かることがあります。さらに汎用的になることで再利用できる機会も増えます。

    もちろん状況によりですが、いろいろな手法を知るという意味で(あくまで学習)として、あえて引数を入れてみるのも面白いと思いますよ!

    C#
    private void RefreshView(List<CellInfo> cells)
    {
        DrawLabelsText(cells);
    
        // 引数で必要な情報を明示的に渡す
        DrawPlayerLabel(m_Board.CurrentPlayerText, m_Board.CurrentColorText, m_Board.CurrentColor);
        DrawStoneCount(m_Board.BlackStone, m_Board.WhiteStone);
    }
    
    C#
    private void DrawPlayerLabel(string playerText, string colorText, Color color)
    {
        Label turnPlayerLabel = TurnLabelPanel.Controls
            .OfType<Label>()
            .FirstOrDefault(label => label.Name == "TurnPlayerLabel");
    
        Label turnColorLabel = TurnLabelPanel.Controls
            .OfType<Label>()
            .FirstOrDefault(label => label.Name == "TurnColorLabel");
    
        if (turnPlayerLabel != null) turnPlayerLabel.Text = playerText;
        if (turnColorLabel != null)
        {
            turnColorLabel.Text = colorText;
            turnColorLabel.ForeColor = color;
        }
    }
    
    C#
    private void DrawStoneCount(int blackCount, int whiteCount)
    {
        Label blackStoneLabel = CountBlackStonePanel.Controls
            .OfType<Label>()
            .FirstOrDefault(label => label.Name == "CountBlackStoneLabel");
    
        Label whiteStoneLabel = CountWhiteStonePanel.Controls
            .OfType<Label>()
            .FirstOrDefault(label => label.Name == "CountWhiteStoneLabel");
    
        if (blackStoneLabel != null) blackStoneLabel.Text = blackCount.ToString();
        if (whiteStoneLabel != null) whiteStoneLabel.Text = whiteCount.ToString();
    }
    
  3. @shirakana

    Questioner

    追加も丁寧にありがとうございます!
    読み込んでよく理解してみます

DrawStoneCountの処理ですが、何故 CountBlackStonePanel なのに、label.Nameが"CountBlackStoneLabel"なんでしょう?
CountBlackStonePanelCountWhiteStonePanelも持っている石数のラベルが一個しかないなら、その名前は"CountStoneLabel"でよくないですか?
もし、そこのラベル名が一致しているならここの処理は下記のように書けるようになります。

private void RefreshView(List<CellInfo> cells)
{
    DrawLabelsText(cells);
    DrawPlayerLabel();
    DrawStoneCount(CountBlackStonePanel, m_Board.BlackStone.ToString());
    DrawStoneCount(CountWhiteStonePanel, m_Board.WhiteStone.ToString());
}
private void DrawStoneCount(Panel panel, string count)
{
    Label countStoneLabel = panel.Controls.OfType<Label>().FirstOrDefault(label => label.Name == "CountStoneLabel");
    countStoneLabel.Text = count;
}

こちらのプログラムでは「メソッドに分けた意味」が生まれたのが分かりますか?メソッドの中身をそのまま呼び出し場所に写しただけでは動かなくなりましたよね?
あなたのDrawStoneCountは「黒石のパネルの黒石の個数のラベルに黒石の個数のテキストを設定し、白石のパネルの白石の個数のラベルに白石の個数のテキストを設定する」という処理を行なっているのに対して、こちらのDrawStoneCountは「指定されたパネルの石数のラベルに石数のテキストを設定する」ということしかしていません。
「1つのメソッドは1つの処理だけ行うべき」というのはこういうことです。

ちなみに個人的にはDisplayNameとかCountStonePanelとかStoneCountとかStoneColorとかを持ったPlayerってクラスか構造体を作って、そいつにDrawPlayerLabelやDrowStoneCountを任せるのが好みです。
その場合は下記のような感じで返り値も引数もないメソッドを呼ぶことになります。

private void RefreshView(List<CellInfo> cells)
{
    DrawLabelsText(cells);
    m_Board.CurrentPlayer.DrawPlayerLabel();
    BlackPlayer.DrawStoneCount();
    WhitePlayer.DrawStoneCount();
}

そういう意味では「引数と戻り値どちらかはあるように組んでみるべき」という考え方自体が「手続型プログラミング」の思考だと思うんで、その講師さんもなんだかなぁという気はします。

1Like

話がよくわからんですが,

MainFormというのは入力手段であって表示手段であるのだとして……

仮に,「やっぱこのオセロはコンソールアプリとして作ろうぜ!」とか方針変更になった(=入力と表示の手段がコンソールになった)ときに,
そしたらもうプログラム全体を完全に一から作り直すような作業になる感じでしょうか?
それとも「オセロの内容の部分」みたいな実装部分は丸ごと使い回せて入力と表示だけをすげ替えてやれば済む感じでしょうか?
後者の側になるような形というのを想像してみると良いのかもしれません.

「内容の部分」側と「入力と表示」側とが なんとなく/相応に/etc 分かれていれば,
「両者の間で何か情報をやり取りする=引数とか戻り値とかを使う」という形が生じるんじゃないかな,的な.

1Like

なんとなくですが・・・

private void RefreshView(List<CellInfo> cells)
{
    DrawLabelsText(cells);
    DrawPlayerLabel();
    DrawStoneCount();
}

これは、

private void RefreshView()
{
    boardView.Refresh();
    playerView.Refresh();
    scoreViiew.Refresh();
}

のようにポリモーフィズムしたくなります。

0Like

Your answer might help someone💌