web-dev-qa-db-ja.com

プロパティの暴走に関する問題

書き換えられた質問

私はフィードバックに感謝し、それに応じて質問を書き直しています。特定の状況(クラスなど)を提供することはできません。また、それ以外の人にはあまり意味のない非常にニッチな領域で働いているので、それが役立つとは思いませんが、より具体的なものを与えるために、似ているが発明された類似の状況を使用します。

ここに、アプリケーションと2つの対象ライブラリーがあります。これらはかなり成熟しており(約10年前)、その長い間販売されてきた製品に関連しています。アプリケーションは、画像を含むさまざまなタイプのファイルを読み取り、それらを検索および表示できるようにします。また、詳細なレポートも作成します。

1つのライブラリ(ImageIO)が画像の読み取りを担当します。 JPEGやPNGを読み取るだけでなく、長年に渡って遭遇した何百もの異なるフォーマットを読み取ります。フォーマットは継続的に追加されています。また、PNGやJPEGなどの標準形式を出力することもできます。

別のライブラリがレポートを担当します。画像だけでなく、あらゆる種類のファイルを処理します。使用されているすべてのメタデータのリストを含む詳細なレポートを提供します。

コードを渡されたとき、メインアプリケーションにはDocumentというクラスがあり、その中には特にImagesのリストが含まれています。 Imageには、HeightWidthGetBitmapなどのいくつかの設定されたプロパティとメソッドがあります。画像の種類ごとに独自のサブクラスがあります。 JpegImagePngImageTiffImageDicomImageなど。これらのほとんどにはカスタムプロパティがあります。使用するカメラ、白色点、色空間、タイトル、GPS位置など。ほとんどの場合、1〜6個の追加プロパティがあります。一部のプロパティは多くのタイプ(exifデータなど)に共通ですが、多くの画像タイプ、特によりニッチなタイプ(BobsImageなど)は、その画像タイプに固有のプロパティを持っています。

_Image

// Some methods
int[][] GetBitmap()

// Some properties
int Height
int Width
_

メインアプリケーションは、これらのプロパティが存在する場合にそれらのいくつかのみを使用します。レポートライブラリはそれらについてレポートしますall。プロパティは数十あります。 no特別なメソッドがありますが、舞台裏では、一部のタイプは標準メソッドのいくつかのプロパティを使用しています。たとえば、ビットマップを生成するためにアスペクト比を使用します。

アプリケーションは、マジックストリングを使用して、画像が実際にどのサブクラスであるかをレポートライブラリに通知します。レポートライブラリはそれを使用してImageをそのサブクラスにキャストし、ifsとスイッチのヒープをそれに応じてレポートするようにキャストします。

このアーキテクチャには満足できませんでした。私の最初の試みは、ImageをとIImageインターフェースに変換し、プロパティをグループにバンドルして、追加の関連インターフェースを用意することでした。 IImageは正常に動作するようですが、プロパティが問題です。プロパティとほぼ同じ数のインターフェイスがあり、それらは「is a」スタイルのテストでテストされました。これは、switchステートメントにかなり戻ったように感じました。

_IImage

// Some methods
int[][] GetBitmap()

// Some properties
int Height
int Width

IGps

Double[] GetGps()
_

私の2番目の試みは、IImagebool HasProperty(PropertyId id)T GetProperty<T>(PropertyId)を追加することでした。その後、他のインターフェイスは必要ありませんでした。

_enum PropertyId
GpsData,
ExifData, ...


IImage

// Some methods
int[][] GetBitmap()

// Some properties
int Height
int Width

// New methods
bool HasProperty(PropertyId id)
T GetProperty<T>(PropertyId)
List<PropertyId> GetSupportedProperties()
_

これにより、レポートライブラリが完全にクリーンアップされました。 GetSupportedPropertiesを列挙でき、ifやスイッチはありません。また、何百ものサブクラスを気にする必要がなく、実際にはサブクラスも必要ありませんでした。 IImageを実装した汎用のImageクラスは、プロパティのリスト、実行時の型チェックの型、および値を含むだけで作成できます。

それでも気分が悪い。コンパイル時の型チェックを削除します。たとえば、var gps = GetProperty<string>(PropertyId.Gps)はコンパイルされますが、Gpsは文字列ではなくdouble配列です。したがって、実行時に例外がスローされます。

また、Flaterは私がインターフェイスのポイントを汚していると指摘し、彼は完全に正しいです。私がこの質問をするのは、私の答えが汚いと思うからです。それは私が持っている最も汚い答えではありません。 1番目と2番目のアプローチはさらに悪いように見えました(元のアプローチははるかに悪いようでした)。

ソリューションは、プロパティの追加を簡単に処理できることが好ましいでしょう。使用するデータイメージ形式を決定することはできません。単一の画像フォーマットは作成していません。仕様(PNGなど)から取得するか、出力形式の約95%と同様に、リバースエンジニアリングを行います。それが私たちのソフトウェアがもたらすメリットです。まれなファイルタイプ(画像形式を含む)を理解し、表示し、レポートします。私たちの時間の約70%は、リバースエンジニアリングが可能な新しいフォーマットにリバースエンジニアリングすることに費やしています。

リバースエンジニアリングは、前向きな計画を本当に妨げます。保存されているデータの一部を信じるのは難しいかもしれません。私は常に驚いており、これを10年以上続けています。これは、私たちが先を見越して行動することができないので、私たちは事後対応する必要があることを意味します。

インターフェイスのツリーを使用した場合(IImageから継承するか、必要に応じて他のものから継承するかは気にしません)、インターフェイスの数が画像の種類やプロパティよりも少ないことに気付きましたが、それでも数十です。また、オブジェクトがインターフェースを実装しているかどうかを確認することは、HasPropertyを呼び出すよりもずっと気分がよくありませんが、おそらくそれは私自身の主観的な問題です。

Flaterの提案は私の最初の試み(2番目のモデル)と少し一致しているようで、Simon Bは私の現在の2番目の試み(3番目のモデル)が最も良いと示唆しています。私はこれを間違って読んでいた可能性があります。どちらかが真実なら、私は汚い気持ちで生きます。まだ見つけられていないのですが、もっと良い方法があるはずだと感じました。

偽物(ただし、ほんの少しの偽物)が役立つが、私はコンテキストを望みます。はじめてよくわからなくなってすみませんでした。私はこれがより良いことを望みます。人々が助けてくれた時間に感謝し、最終的には答えを受け入れます。


参考のために保存されている古い質問

私は臭いクラスをリファクタリングしていて、豚の耳を作っていると確信しています。よくある問題のようですが、よくある解決策がわかりません。ドメインはかなりニッチなので、名前などを変更しました。

インターフェースがあります。たとえば、いくつかのメソッドがあり、いくつかのプロパティで始まるIThingとしましょう。時が経つにつれ、さまざまなIThingsがさまざまなプロパティで作成されました。 (IThingは、私たちが制御できない複数の異なるリバースエンジニアリングされたThingへの一種のインターフェースなので、プロパティは私たちに押し付けられます。)

最終的には、_bool HasSpecialNumber_、_int SpecialNumber {get; set;}_のようなパターンになりました。これは、プロパティを追加するにつれて臭くなりました。すべての実装では、プロパティをサポートしていないというだけで、20以上のメソッドを実装する必要があります。

私はミックスインアプローチを使用することを考えましたが、プロパティまたはプロパティの組み合わせと同じ数のインターフェイスと多くのキャストが含まれるため、これを間違って考えている可能性があります。ここでプロパティのみを指定していて、メソッドが変更されていない場合も、強引です。

IThingは次のようになります(C#ish疑似コード)

_IThing

// Some methods every Thing supports
DoSomething
DoSomethingElse

// A bunch of properties some Things support
bool HasSpecialNumber { get; }
int SpecialNumber { get; }

bool HasName { get; }
string Name { get; }

... and so on
_

匂いは別として、物件が追加されるたびに、たくさんのクラスが壊れました。これらもすべて、protobuf-netを使用してシリアル化する必要がありました。これらのクラスの多くは、特別なオブジェクトを持っているという点でのみ異なっていました。

次に試みたのは、プロパティを2つのメソッドに減らし、プロパティを追加するためのプライベートメソッドを使用することです。

_IThing

// Some methods every Thing supports
DoSomething
DoSomethingElse

// A bunch of properties some Things support
bool HasProperty( PropertyIdEnum propertyId )
T GetProperty<T>( PropertyIdEnum propertyId )

// Private method for adding properties
void AddProperty<T>( PropertyIdEnum propertyId, T value )

_

この種の作品。数十のプロパティが2つのアクセサメソッドになり、PropertyIdEnumを更新しても何も壊れませんでした。 AddPropertyは、IDをオブジェクトにマップする辞書にプロパティを追加するために使用され、奇妙なキャストエラーが発生しないようにTypeが一緒に保存されました。 ただし、コンパイル時の型チェックと実行時の型チェックを交換しました。また、protobuf-netはシリアル化をサポートしていませんObjects orTypes、ただし、これは実装の詳細です。

AddProperty抽象化を廃止し、数十のクラスに戻りました。これにより、心配するクラスが多くなる代わりに、protobuff-netの問題が解決しました。コンパイル時の型安全性はまだ不足しています。

この問題は、私が働いている地域のいたるところで見られます。たとえば、ffmpegとそれらが扱うCODECは、それぞれ特別な動作をします。彼らが使用するソリューションは後方互換性によって制約されていますが、私はC#を使用している間、大幅に最適化されたCで動作しています。単一の共通インターフェースを介して処理する必要のある一連のプロパティを処理するためのパターンまたはアドバイスはありますか?プロパティを制御できたとしても、そもそもこのような状況にはならないでしょうが、そうではないので、ここにいます。

3
timbo

通常の解決策は放棄したものであり、一般的に「継承よりも構成を優先する」として要約されています。

特別な値を保持するコンテナを持つ単純なクラスを作成します。そのコンテナは、事実上名前と値のペアのコレクションである辞書/マップである可能性があります。適切なコンテナを使用している場合、クエリはかなり高速になります。

3
Simon B

メタプログラムを作成しようとしていて、実際に解決するよりも多くの問題が発生します。 (まだ定義されていない)プロパティセットを受け取るインターフェイスを作成しようとしています。これにより、インターフェイスの目的が損なわれます。つまり、クラスが特定のプロパティセット(またはメソッド)を実装するように強制します。

いくつかのモノがサポートする一連のプロパティ

インターフェイスはクラスmustが持っているものをリストする必要があります。それが可能なものではありませんでした。それはインターフェースの目的ではありません。これはLiskov( [〜#〜] lsp [〜#〜] )違反の異常なバリエーションであり、実際に特定のプロパティが含まれているかどうか、またはではない:

if(myThing.HasSpecialNumber)
{
    // do something with myThing.SpecialNumber
}

2回目の試行では上記の構文を書き換えますが、実際に常に確認する必要がなくなるわけではありません。

静的に型付けされた言語は、オブジェクトの構造を知るに重点を置いており、それを推測/チェックする必要はありません。インターフェイスを分割し、インターフェイスのコントラクトを満たすクラスでのみ実装し、他のクラスでの偽の実装は行わないでください。

以下に沿ったもの:

public interface IThing
{
    // Some methods every Thing supports
    void DoSomething();
    void DoSomethingElse();
}

public interface IThingWithName : IThing
{
    string Name { get; set; }
}

public class ThingWithoutName : IThing
{
    public void DoSomething() 
    {

    }

    public void DoSomethingElse()
    {

    }
}

public class ThingWithName : IThingWithName 
{
    public void DoSomething() 
    {

    }

    public void DoSomethingElse()
    {

    }

    public string Name { get; set; }
}

これにより、特定のクラス(ThingWithoutName/ThingWithName)、およびそのインスタンスのすべてが、事実についてNameプロパティかどうか、したがって、そうであるかどうかを確認し続ける必要はありません。

これにより、C#が実行するように構築されていないランタイムオブジェクト評価の代わりに、強力で静的な型付けが確実に行われます(技術レベルで行うことができますが、多くの欠点を持つ悪いアプローチです)。

いくつかの脚注:

  • IThingWithNameは明らかにすばらしい名前ではありませんが、あなたの質問には、ここで適切な名前を選択するのに十分なコンテキストがありません。
  • インターフェイスを継承する必要があるのか​​、別のインターフェイスを定義して両方をクラスに実装する必要があるのか​​は不明です。これは、質問で指定されていないコンテキスト上の考慮事項です。質問のフレーズに基づいて正しいオプションを選択したと思いますが、100%確実ではありません。
  • すべてのプロパティに固有のインターフェースが必要になる場合は、おそらく何かがおかしいでしょう。しかし、繰り返しになりますが、質問はコンテキストについて軽すぎて、これについて明確に述べることはできません。
6
Flater

あなたの問題:約100種類の画像クラスがあります。いくつかは、6つ言うと、 "EXIF"プロパティを持っています。ライブラリのユーザーは、自分の画像を処理する画像クラスを気にせず、画像に「EXIF」プロパティがあるかどうかを知り、EXIFプロパティを読み取るか変更したいと考えています。また、ライブラリのユーザーはビルド時にイメージが何であるかを認識していないため、これは実行時に行う必要があります。

1つのアプローチは、ユーザーには「Image」クラスのみが表示され、「Image」クラスには3つのメソッド「hasExif」、「getExif」、「setExif」があることです。 「hasExif」はtrueまたはfalseを返し、「getExif」はデータを取得するか、例外をアサート/スローします。「setExif」はデータを設定するか、例外をアサート/スローします。 Imageクラスには、EXIFセッターおよびゲッターの実装へのポインターがあります。プロパティをサポートする画像クラスはそれらを設定しますが、他のクラスは設定しません。

たくさんのコードになりますが、すべて非常に単純でクリーンなコードです。 2ダースの類似したプロパティを持つことは複雑ではありません。

2
gnasher729

私の質問を適切に理解した場合、問題は画像処理にまったくありません。プロパティセットのバリエーションが非常に多い場合、レポートアプリにプロパティの完全なセットを提示するだけです。

これには非常に簡単な解決策があります:辞書(別名マップ)を使用します。そのようなGetProperties()(または何か)と呼ばれる単一のメソッドを、すべての画像クラスが実装する基本インターフェースに追加します。各タイプの画像は、そのプロパティをレポートレイヤーに提示できます。次に、レポートレイヤーはすべてのプロパティを単純に反復して表示できます。

文字列のキーと文字列の値のディクショナリがあれば十分です。これは、これらのプロパティのいくつかが複雑である場合(たとえば、複数の値で構成されている場合)に問題となる可能性があり、さまざまな方法でそれらを表示できるようにする必要がある場合に、画像クラスでそれらを表示する方法の仕事を残します。その場合、プレゼンテーションオブジェクトを受け入れ、そのオブジェクトのメソッドを呼び出してプロパティを説明する2番目のインターフェイスを定義すると便利です。現時点では時間に余裕はありませんが、詳細については、ご要望に応じてご連絡いたします。

1
JimmyJames

tl; dr基本的に古いソースコードを逆コンパイルしているように聞こえます。逆コンパイルは厄介な結果を生む傾向があるため、厄介な結果が得られることは予見可能な結果のように思えます。したがって、ボトムアップのリファクタリングの代わりに、古いコードが実装の詳細の記入に役立つトップダウンの再設計が必要になる可能性がありますが、全体的な論理構造を提供するためにそれに頼るべきではありません。


私の疑いは、あなたがトップダウンアプローチではなくボトムアップアプローチをしていることです。

たとえば、強力な最適化を有効にしてネイティブコードにコンパイルされる適切に設計されたプログラムを考えてみます。次に、誰かがそのコンパイル済みプログラムを受け取り、 逆コンパイル します。入力プログラムの明確でエレガントな構造に関係なく、その逆コンパイルは実際に混乱する可能性があります。

したがって、私の疑いは、この古いプログラムをリファクタリングするときに、逆コンパイラのようにやっているということです。トップダウンのアプローチをとるのではなく、古いソースコードを1つずつ逆コンパイルしようとして、同じ結果になります。デコンパイラが通常生成する一種の混乱。

基本的に、リファクタリングを1つずつ行うことはできません。代わりに、トップダウンアプローチのように、すべてを説明できる一般的な論理構造を把握し、そこから派生する必要があります。これは、古いプログラムの逆コンパイルではなく、新しいプログラムを新しいプログラムとして記述します。

具体的には、インターフェースとプロパティの問題を忘れてください。代わりに、たとえば画像を説明する全体的なロジックを構造化する方法に焦点を当てます。それらすべてを含む抽象化を理解し、詳細に絞ってサブタイプを構築します。論理的な構造が整ったら、古いコードを利用して実装の詳細を記入できます。

さもなければ、逆コンパイラの作業を行うのに行き詰まってしまい、厄介な最終製品が予見可能な結果を​​もたらすことになるでしょう。


思考実験:逆コンパイラがそのような厄介な結果を生成するのはなぜですか?

特に元のプログラムが非常に高級な言語で書かれていて、積極的な最適化でネイティブコードにコンパイルされた場合は特に、逆コンパイルを調べて、逆コンパイラが適切なソースコードを正確に再現しない理由を検討することをお勧めします。 。

ハンドルを取得したら、コンパイル済みのネイティブコードとしてリファクタリングするソースコードを確認します。つまり、ええ、あなたはすでにC#にあるものをリファクタリングしているかもしれませんが、C#はそうではありません実際のソースコード; realソースコードは元のソフトウェア設計者の頭の中に存在し、それをC#にコンパイルしました。この心からC#へのコンパイルは、C#をネイティブコードにコンパイルするのと同様に、損失の多いプロセスです。そして、逆コンパイラがコンパイルで失われた構造を取得しないように、コードを最初に書いた人がそれをC#にコンパイルしたときに失われた構造を取得しません。

あなたは本当に修正することができません。残念ながら、元のプログラマーがいなくなったら、元のソースコードも消えてしまいます。入手したC#は、かつて誰かの頭の中に存在していた実際の完全な仕様の代わりにはなりません。

要するに、私の提案は、リファクタリングのアプローチがデコンパイラーが行うことと似ていることに焦点を当てることです。それがわかったら、それを修正する方法はもっと明白になるはずだと思います。

0
Nat

ここでの問題は、カプセル化の欠如です。インターフェイスは、プロパティよりもメソッドを公開することを選択する必要があります。呼び出しコードがこれらの各実装のプロパティにアクセスすることにより、実装から呼び出し元に知識が漏れます。

レポートの責任を各実装に委任することは可能ですか?その場合、インターフェースにGenerateReportメソッドがあり、呼び出し元はクラスのプロパティを知る必要はありません。

クラスを使用する他の方法についても繰り返します。責任がより明確になる場合があります。


静的な型チェックを失うため、辞書ソリューションはうまくいきませんが、そもそもそれらのプロパティは一般公開されるべきではありません。それらがプライベートになると、静的型付けは自然に機能し、フォーマットクラス自体にフォーマット固有の知識を保持する方法を見つける必要があります。

0
Jared Goguen

Swift、およびおそらく他の言語では、別のアプローチが可能である可能性があります:Swiftでプロトコルを定義します(ほぼインターフェースと同じ)2つのメソッドgetExifとsetExifでExifHandlerを言います。任意のクラスまたは構造体独立して、ExifHandlerをサポートすることを宣言できます。プロパティにアクセスするコードは次のようになります。

If let handler = image as? ExifHandler {
    let exif = handler.exif
    ...
    handler.exif = newExif
}

または:

If let exif = image as? ExifHandler? .exif {
   ...
}
0
gnasher729