六角形のグリッドを処理するオープンソースライブラリを書いています。これは主にHexagonalGrid
クラスとHexagon
クラスを中心に展開します。 HexagonalGridBuilder
オブジェクトを含むグリッドを構築するHexagon
クラスがあります。私が達成しようとしているのは、ユーザーが各Hexagon
に任意のデータを追加できるようにすることです。インターフェイスは次のようになります。
public interface Hexagon extends Serializable {
// ... other methods not important in this context
<T> void setSatelliteData(T data);
<T> T getSatelliteData();
}
ここまでは順調ですね。 HexagonalGridCalculator
という名前の別のクラスを作成しています。これは、2つのHexagon
s間の最短パスの計算や、Hexagon
の周囲の視線の計算など、ライブラリにいくつかの凝った計算を追加します。私の問題は、Hexagon
を通過するcostや、オブジェクトがtransparent/passable)であるかどうかを示すHexagon
フラグなど、boolean
オブジェクトのデータをユーザーが提供する必要があることです。かどうか。
私の質問は、これをどのように実装する必要があるかです。
私の最初のアイデアは、次のようなインターフェイスを作成することでした。
public interface HexagonData {
void setTransparent(boolean isTransparent);
void setPassable(boolean isPassable);
void setPassageCost(int cost);
}
そして、ユーザーにそれを実装させますが、後で他の機能を追加すると、古いインターフェースを使用している人にとってはすべてのコードが壊れてしまうことに気づきました。
だから私の次のアイデアは次のような注釈を追加することです
@PassageCost
、@IsTransparent
および@IsPassable
これはフィールドに追加でき、計算を実行しているときに、ユーザーが指定したsatelliteData
で注釈を探すことができます。後で変更される可能性を考慮に入れると、これは十分に柔軟に見えますが、リフレクションを使用します。アノテーションを使用するコストのベンチマークがないので、ここでは少し暗闇にいます。
ほとんどのユーザーはこれが重要なグリッドを使用しないため、90〜95%のケースでは効率は重要ではないと思いますが、誰かが5.000.000.000 X 5.000.000.000
のサイズのグリッドを作成しようとしていると想像できます。
では、どの道を歩き始めるべきでしょうか?または、いくつかのより良い選択肢がありますか?
注:これらのアイデアはまだ実装されていないので、良い名前にはあまり注意を払いませんでした。
このように明確に定義されていない問題については、インターフェイスがどうなるかを特に参照せずに、最初に実際のアプリケーションを作成することを好みます。もちろん、インターフェイスがどのようになるかを考える必要がありますが、クライアントは1つしかないため、必要に応じて自由に再編成できます。
インターフェイスを使用する2番目のアプリケーションを起動するときは、最初のアプリケーションによって提供されるメソッドから2番目のアプリケーションに必要なメソッドの抽象化を開始します。繰り返します。 2番目のアプリケーションは、必然的に最初のアプリケーションを強制的に変更して、共通のインターフェイスをサポートします。 3番目は、2番目と1番目を強制的に変更します。等。
新しいアプリケーションを追加するときにインターフェイスを変更する必要がなくなったら、その後インターフェイスを公開する準備が整います。
私の最初のアイデアはインターフェースを書くことでした[...]しかし、後で他の機能を追加すると、古いインターフェースを使用している人にとってはすべてのコードが壊れてしまうことに気づきました。
代わりに抽象クラスを使用してください。
public interface Hexagon<T extends HexagonData>{
T getData();
void setData(T data);
}
public abstract class HexagonData{
public boolean isTransparent(){ return false; }
public boolean isPassable(){ return true; }
public int getPassageCost(){ return 0; }
}
注釈が付けられたメンバーに関するメタデータが実際に必要でない限り、注釈を使用しないでください。
HexagonalGridCalculatorなどの他のHexagonHandlerを変更せずに、データ要素/プロパティ/変数をHexagonに追加する場合は、次のような動的プロパティを追加できます。
public class Hexagon implements Serializable {
// ... other methods not important in this context
void setProperty(int id, object data);
object getProperty(int id, object notFoundValue);
}
データ要素/プロパティ/変数の定数を定義します
const int SatelliteData = 1;
後で追加データが必要な場合は、新しい定数を定義するだけです
const int Transparent = 2;
const int Passable = 3;
const int tPassageCost = 4;
これは他のハンドラーには影響しません。
C++標準ライブラリで使用されているものと同様のファセットアプローチを提案します。
interface Hexagon {
void setData(int id, T data); //these should only be used from Facet class
Object getData(int id);
sealed class IFacet<T> {
private static int count = 0;
private int _id = count++;
public T getData(Hexagon hexagon) {
return (T) hexagon.getData(_id);
}
public void setData(Hexagon hexagon, T data) {
hexagon.setData(_id, data);
}
}
}
//Usages:
class CostManager {
public readonly Facet<int> PASSAGE_COST = new Facet<int>();
static void incrementCost(Hexagon hex) {
PASSAGE_COST.setData(hex, PASSAGE_COST.getData(hex)+1);
}
}
class TransparencyManager {
public readonly Facet<boolean> TRANSPARENT = new Facet<boolean>();
static void disable(Hexagon hex) {
TRANSPARENT.setData(hex, false);
}
}
各ファセットは、Hexagonの1つのプロパティのみをアドレス指定します(一意の_idを使用するため)。 Hexagon.setData()およびHexagon.getData()が常にファセットクラスを介して使用される場合、格納されるデータは常に対応するファセットのタイプになります。これにより、ユーザーが特定のIDに間違ったタイプのデータを書き込もうとするのを防ぐことができます。
このアプローチが保護するエラーの種類を説明するために、@ k3bの回答に基づく例を検討してください。
static void configure(Hexagon hex) {
hex.setData(DataType.TRANSPARENCY, "very transparent"); // compiler accepts this just fine, strings are objects, aren't they?
}
static void processHexagon(Hexagon hex) {
boolean data = (Boolean)hex.getData(DataType.TRANSPARENCY); // compiler accepts this, Boolean are objects too
}
static void doSomeJob(Hexagon hex) {
configure(hex);
// working
// more work
// ...
processHexagon(hex); //oops, you've got runtime error
}
次に、ファセットで同じコードを検討します。
static void configure(Hexagon hex) {
TRANSPARENT.setData(hex, "very transparent"); // compiler slaps you
}
static void processHexagon(Hexagon hex) {
boolean data = TRANSPARENT.getData(hex); // no downcast!
}
static void doSomeJob(Hexagon hex) {
configure(hex);
// working
// more work
// ...
processHexagon(hex); //no runtime errors, just compile-time ones
}
ご覧のとおり、現在はダウンキャストの必要がないため、コードはさらに冗長ではありません。
DTOのように構築します。必要なデータのゲッターおよびセッターとのインターフェースを持ち、外部システムで処理を行います。必要に応じて拡張できるデフォルトの16進数を提供することもできます。その結果、オブジェクト指向よりもエンティティコンポーネントシステムが多くなりますが、これは、コンポーネントシステムのポイントであり、事前に予期していなかった方法で拡張できる柔軟性を備えています。