Laden...

[gelöst] Methodenverhalten für komplettes Set von Subklassen ändern

Erstellt von LaTino vor 8 Jahren Letzter Beitrag vor 8 Jahren 1.781 Views
LaTino Themenstarter:in
3.003 Beiträge seit 2006
vor 8 Jahren
[gelöst] Methodenverhalten für komplettes Set von Subklassen ändern

Moin,

ich habe den Eindruck, mein Hirn ist bereits im Wochenende, jedenfalls stehe ich gerade auf dem Schlauch bei einem Problem, dessen Lösung mir auf der Zunge liegt - ich komm aber einfach nicht drauf.

Situation: ich habe mehrere Familien von Klassen, die (unter anderem) ein bestimmtes Interface implementieren. Dazu passend gibt's dann auch die entsprechende abstrakte Fabrik. An beides komme ich nicht ran, dh. weder habe ich Zugriff auf die konkreten Implementierungen der Schnittstellen, noch auf die Implementierungen der Fabriken.

Für die konkreten Klassen benötige ich ein anderes Verhalten einer Methode, das für bestimmte Ausprägungen der Eingansparameter etwas anderes tut, also die eigentliche Implementierung vorsieht. Aaaalso:


public override ResultType DoSomeThing(IInputParameterInterface input)
{
            if(input.SpecialBehaviour)
                return DoSomethingElse();
            return NormalResult();
}

Nun ist mein Ansatz, das von der Fabrik ausgeworfene Objekt mit einem Decorator zu versehen und bis auf den Methodenaufruf oben in alles einfach an das dekorierte Objekt durchzureichen:


public override ResultType DoSomeThing(IInputParameterInterface input)
{
            if(input.SpecialBehaviour)
                return DoSomethingElse();
            return _decoratedMember.DoSomeThing(input);
}

In der Theorie sehr schön, nur hat das Interface (wie gesagt, nicht meins, und auch kein Zugriff drauf), das der Decorator eben auch implementieren müsste, knapp 150 Methoden, von denen mich 149 nicht interessieren.

Also: wie schiebe ich den Klassen, deren konkreten Typ ich zur Laufzeit nicht kenne, eine Methodenänderung für eine einzige Methode unter, ohne die gesamte Schnittstelle implementieren zu müssen? Steh auf dem Schlauch...

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

P
1.090 Beiträge seit 2011
vor 8 Jahren

Also den Typ wirst du zur Laufzeit schon kenne/ bestimmen können.

Dein Problem wird wohl sein, das die eigentliche Implementierung nicht public ist und du so nicht einfach von ihr Erben kannst. Das kann durch aus von den Entwicklern so geplant sein. (Ist jetzt eine Vermutung)

Wenn du nur eine Methode brauchst, erstelle doch einfach für deinen Code ein eigenes Interface und programmiere gegen das. Dann brauchst du auch nur die Methode zu überschreiben.

Ohne jetzt genauer zu wissen worum es da geht, ist es wirklich schwer einen Antwort zu geben.

Sollte man mal gelesen haben:

Clean Code Developer
Entwurfsmuster
Anti-Pattern

S
417 Beiträge seit 2008
vor 8 Jahren

Hallo,

bei dieser Ausgangssituation wirst du wohl über einen dynamischen Proxy nicht herumkommen.
Eine mögliche Library hierfür wäre https://github.com/Curit/DynamicProxy

Basierend darauf zeigt das folgende Beispiel wie du einen Proxy erstellst, die entsprechende Methode abfängst und mit deiner Implementierung ersetzt. Zunächst die Dinge die du kennst, nämlich die Schnittstelle und die Implementierung (auch wenn diese für dich unveränderbar sind):

public interface ITest
{
	string NotInterceptedMethod();
	int DoStuff(int a, int b);
}

public class Test : ITest
{
	public string NotInterceptedMethod()
	{
		return "Hello World";
	}

	public int DoStuff(int a, int b)
	{
		return a + b;
	}
}

Die Standardimplementierung von DoStuff liefert die Addition zweier Zahlen zurück. Wir wollen jedoch eine Multiplikation daraus machen:

public static class Program
{
	static void Main(string[] args)
	{
		ITest test = new Test(); // Objekt für das der Proxy erstellt werden soll
		ITest proxiedTest = ProxyFactory<ITest>.Proxy<ITest>(test);
		var proxy = proxiedTest as IProxy<ITest>;
		proxy.AddInterceptor<int, int, int>((obj,a,b) => obj.DoStuff(a, b), (func, a, b) =>
		{
			// wir fangen den Aufruf von DoStuff ab und ersetzen die Standardimplementierung durch eine Multiplikation
			return a * b;
		});

		var res = proxiedTest.DoStuff(4, 7); // liefert 28 und nicht 11

		Console.WriteLine(res);
		Console.WriteLine(proxiedTest.NotInterceptedMethod()); // zeigt, dass die nicht abgefangene Methode, das Ergebnis aus dem Originalobjekt liefert
		Console.ReadLine();
	}
}
LaTino Themenstarter:in
3.003 Beiträge seit 2006
vor 8 Jahren

Hm, ja, die dynamischen Proxies sind auch "nur" sehr aufgebohrte Wrapper (decorator), zumindest, was die Anwendung für meinen speziellen Fall angeht.

@Palin: nein, mein Problem ist, dass ich ausschließlich gegen das Interface programmiere. Inheritance löst hier das Problem nicht, weil meine veränderte Methode:

  • ihr eigenes Ding durchzieht
  • danach die eigentliche Methode aufruft

Die Interfaces und seine verschiedenen Implementierungen (~30 Stück, die werden so auch gebraucht) sind schon public. Nur darf ich weder das Interface noch die Implementierungen anrühren.

Die konkrete eigentliche Methode ist mir aber nicht bekannt, nur, dass sie aufgerufen wird (und nicht, welche der Implementierungen).


public interface IMainInterface 
{ 
  void Method1();
  void Method2();
  //..
  void Method 150();
}
public class ConcreteImplementation1
{
  public void Method1() { Console.WriteLine("ConcreteImplementation1.Method1 called."); }
  //restliche Implementierungen
}
public class ConcreteImplementation2
{
  public void Method1() { MessageBox.Show("ConcreteImplementation2.Method1 called"); }
}

Das befindet sich alles in einer Assembly, die unverändert bleiben soll, zusammen mit den Factories dafür.

Das, was ich unter meiner Kontrolle habe, ist die Benutzung von obigem Geraffel:


//...
var factory = new ExampleFactory(Config);
IMainInterface exampleInstance = factory.CreateExampleClass();

DoSomethingSpecial();
exampleInstance.Method1();

Und da DoSomeThingSpecial etwas machen soll, dass den Ablauf von Method1 nicht groß ändert, aber

  • auf zwei protected properties von IMainInterface
  • auf ein Zwischenergebnis von Method1()

zugreifen muss, würde ich bei einer Ableitung das hier machen:


public void Method1() {
   DoSomethingSpecial();
   base.Method1();
}

Geht aber nicht, da ich dann
a) von allen 30 konkreten Implementierungen ableiten müsste
b) die Factory so ändern müsste, dass sie mir ein Objekt mit dem veränderten Algorithmus, der in base() aber das für die Situation Passende erstellt. Geht auch nicht.

Ich hab's jetzt in den sauren Apfel gebissen und Decorator benutzt (okay, 10 Minuten lang eine regex gebastelt, die in 150 Methoden ein base.Methode2() eingefügt hat^^).

Sieht so aus:


public class DecoratorClass : IMainInterface
{
    private IMainInterface _decoratedObject;
    public DecoratorClass(IMainInterface decoratedObject) { _decoratedObject = decoratedObject; }

    private DoSomethingSpecial() { /*...*/ }
    public override void Method1() { DoSomethingSpecial(); _decoratedObject.Method1(); }
    public override void Method2() { _decoratedObject.Method2(); }
    //148 mal noch ;)
}

//Instanziierung dann:
var factory = new ExampleFactory(Config);
IMainInterface exampleInstance = new DecoratorClass(factory.CreateExampleClass());

exampleInstance.Method1();

Nichtsdestotrotz bleibt das Problem, dass man nur einen minimalen Teil einer Implementierung für alle Varianten dieser Implementierung ändern will, und das, ohne die eigentlichen Implementierungen zu verändern. Und dass man mit meiner Variante eben einen Haufen redundanten Mist verzapfen muss, für ein paar wenige relevante Veränderungen.

Ich könnte per Reflection rein, aber das ist natürlich keine saubere Lösung.

Wie gesagt, das eigentliche Problem ist gelöst, aber über ein Stichwort zu einer eleganteren Lösung (auch wenn die jetzt nicht so schlecht ist) wäre ich trotzdem zu haben 😉.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

P
1.090 Beiträge seit 2011
vor 8 Jahren

Wenn Konkreten Implementierungen public sind, dann kann deine Klasse von ihnen Erben. Dann brauchst du auch nur die Methoden Überschreiben die du ändern Möchtest.

Um die Factory kannst du einen Wrapper legen. Der dann Ansteller der "Orginal" Klasse, einen passende Instanz deiner Klasse zurück gibt.

Sollte man mal gelesen haben:

Clean Code Developer
Entwurfsmuster
Anti-Pattern

LaTino Themenstarter:in
3.003 Beiträge seit 2006
vor 8 Jahren

palin, ich soll von 30 Klassen ableiten? Sicher? Und ich BRAUCHE die Originalklasse mit ihrer Implementierung.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

S
417 Beiträge seit 2008
vor 8 Jahren

Ich verstehe nicht, was an meiner genannte Variante mit Dynamic Proxy so "unelegant" ist.
Es ist weniger Aufwand als mit Regex und zudem wiederverwendbar.
Die Anforderungen (d.h. kein Zugriff auf die Implementierung noch auf die Factory) geben nicht viel weitere Möglichkeiten her als einen Wrapper zu nutzen.

P
1.090 Beiträge seit 2011
vor 8 Jahren

Nun wenn du einen Generischen Ansatzt hast, hast du die Implementierung nur einmal. Und ja dann 30 Klassen, die Von ihr erben. Wenn es jetzt in der Basis Klasse einen neue Methode gibt, hast du sie auch Automatisch bei dir drin und brauchst sie nicht in deiner Klasse nachzu ziehen.
Und von 30 Klassen erben ist halt wenige als 150 Methoden zu überschreiben.

Ich denke mal du brauchst die Orginal Instanz (die Klasse hast du ja durch die Vererbung), weil die Factory dort irgendwelche werte Setzt. Hier wirst du dann die Passenden Werte des Objekts, kopieren müssen. (Wenn die Klasse Json Serialisibar ist, denke ich es könnte mit einem 2 Zeiler klappen).

Und natürlich kann ich nicht sicher sein. Da ich einfach nicht genau weiß was du machts. Wir können die Möglichkeiten vorstellen. Aber im entefekt wirst du dich entscheiden müssen was du machst.

Sollte man mal gelesen haben:

Clean Code Developer
Entwurfsmuster
Anti-Pattern

LaTino Themenstarter:in
3.003 Beiträge seit 2006
vor 8 Jahren

@Palin, sorry, ich glaube, wir reden aneinander vorbei.
Die Factory setzt keine Werte, sie liefert mir die für die Situation geeignete Implementierung der Schnittstelle. Die 150 Methoden überschreibe ich mit "_decoratedObject.MethodenNameeinfügen". Mit Generik hat das ganze auch nichts zu tun. Vererbung ist schon aus dem Grund keine Lösung, weil, wenn jemand vom anderen Team eine 31. Klasse implementiert, ich davon nichts erfahre und der Crash unvermeidlich wäre.

@sarc sie beinhaltet das einbinden einer Drittbibliothek. Das bedeutet Prüfung auf Lizenzrecht, Sicherheitsaudit und Freigabe durch die QS. Dein Vorschlag ist nicht schlecht, aber ein ziemlich großer Overkill.

Wie gesagt, gelöst ist das Problem ja erst einmal, funktioniert gut, und hat null Wartungsaufwand. Nur bin ich grundsätzlich faul und hätte nach einer schnellen Möglichkeit gesucht, das Verhalten einer Methode einer Familie von Klassen gleichzeitig zu beeinflussen (die derzeitige Lösung hat das Potenzial, das Verhalten ALLER Methoden einer Familie von Klassen zu beeinflussen, also ein kleiner Overkill).

Wenn es interessiert, kann ich ja eine kleine Version des Problems mal zur Verfügung stellen. Ist in jedem Fall 'ne gute OOP-Fingerübung.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

LaTino Themenstarter:in
3.003 Beiträge seit 2006
vor 8 Jahren

So, ich habe die Hälfte der Mittagspause investiert 😉.

Anbei ein kleines Consolen-Projekt. Es benutzt eine ZooLib.dll, deren Quelle ich mal weglasse, weil's ja grade darum geht, dass man auf die konkreten Klassen keinen Zugriff haben soll.

Ist jetzt nicht perfekt, aber die mir vorliegende Struktur wird recht gut abgebildet, wenn auch im kleineren Maßstab.

Jede der konkreten Klassen (Bear, Antelope, Lion, Penguin, Eagle, Otter) hat einen internen Zähler _sleepCounter, der bei jedem Aufruf von Sleep(). hochgesetzt wird.

Anforderung: für alle Tiere soll eine Warnung ausgegeben werden, wenn Sleep() aufgerufen und dieser Sleepcounter > 3 ist.
Problem: es können jederzeit neue konkrete Klassen hinzukommen.

Lösung: wie oben beschrieben ein Decorator (Wrapper), Nachteil: da er auch IZooInhabitant implementiert, müssen auch die Methoden implementiert werden, bei denen sich nichts ändert.

Frage: gibt es eine Lösung, die nur und ausschließlich die zu ändernde Methode betrifft? Ich neige langsam (nach vier Tagen, in denen ich immer mal drüber nachdenke) dazu, dass die Antwort "nein" ist 😉.

Download (50 kB)

Grüße,

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

16.842 Beiträge seit 2008
vor 8 Jahren

Versteh ich das Problem nicht, oder willst Du einfach nur

public abstract class ZooInhabitant : IZooInhabitant
{
 // Überschreibbare Methoden hier; deckt sich mit der Implementierung von IZooInhabitant
 public virtual void Add()
 {
	// XYZ
 
 }
}

public class Lion : ZooInhabitant
{
 public override void Add()
 {
	// XYZ
 }
}
LaTino Themenstarter:in
3.003 Beiträge seit 2006
vor 8 Jahren

Ich will, dass


myPet.Sleep();

eine Consolenwarnung ausgibt, wenn der interne Zähler von myPet (_sleepCount) größer als 3 ist.
Hängt euch mal nicht an dem Consolenprojekt auf, das ist quasi nur, um herumspielen und austesten zu können.

Das herumfummeln an den konkreten Klassen ist ausdrücklich nicht möglich. Es soll auch nciht von allen konkreten Klassen je eine Ableitung gebildet werden, weil jederzeit neue dazukommen können und man dann wieder eine neue Ableitung machen müsste.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

F
10.010 Beiträge seit 2004
vor 8 Jahren

Nur über einen Proxy.

Ob du das mit einem DynamicObject oder nem echten Procxy machst ist dann "Geschmackssache".

LaTino Themenstarter:in
3.003 Beiträge seit 2006
vor 8 Jahren

Hmkay. So hab ich's instinktiv gemacht, die anfallende Tipp-/RegEx-Find-and-Replace-Fleissarbeit mag mir vorgegaukelt haben, es gäbe noch etwas, an das ich mich nicht erinner 🤔

Danke dir.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

F
10.010 Beiträge seit 2004
vor 8 Jahren

Mit den Proxies ( wie ihn z.b. sarc vorstellte ) musst du nicht alles von hand machen.
Es reicht dann lediglich das abzufangen was du anders machen willst.

Und den RealProxy gibt es schon seit FW 1


class MyProxy<T> : RealProxy
{    
    T target;

    public MyProxy(T target)
        :base(typeof(T))
    {
        this.target = target;
    }
    
    public override IMessage Invoke(IMessage msg)
    {
        var message = msg as IMethodCallMessage;
        var methodName = (String)msg.Properties["__MethodName"];
        var parameterTypes = (Type[])msg.Properties["__MethodSignature"];
        var method = typeof(T).GetMethod(methodName, parameterTypes);
        var parameters = (Object[])msg.Properties["__Args"];

// hier ggf was einfügen
        var response = method.Invoke(target, parameters);

        return new ReturnMessage(response, null, 0, null, message);
    }
}

Ist dann nicht wirklich viel arbeit.

LaTino Themenstarter:in
3.003 Beiträge seit 2006
vor 8 Jahren

Ich merke grad, wir reden von verschiedenen Dingen bei "Proxy". Oder genauer, von Dingen, die dieselbe Arbeit auf unterschiedliche Weise machen (selbe Arbeit -> beides ist ein Proxy).

Die Sache ist, dass ich Dinge wie Reflection (und den RealProxy, von dem ich tatsächlich nicht wusste, dass so etwas im fw ist oO) für OOP-feindlich halte. Das ist nicht immer eine schlechte Sache, deinem Beispiel sieht man aber die Brechstange schon von weitem an, sozusagen.

Sich an das Zielobjekt hängen, in den Methodenaufruf einbrechen und ihn hart durch etwas vollkommen anderes ersetzen, meine ich damit. Das widerstrebt mir, weil's normalerweise fast immer eine Lösung gibt, die sich aus den OOP-Prinzipien ergibt.

Bei genauerem Nachdenken sagen die allerdings auch, dass eben genau die Manipulation nur von Teilen eines "lebenden" Objekts nicht stattfindet. Dh die Lösung besteht entweder darin

  • ein Manipulationsobjekt vollständig zu implementieren (was ich gemacht habe, also Proxy-Pattern) und dabei die Kapselung der Objekte zu beachten oder
  • die Kapselung zu ignorieren und die Manipulation einzelner Aspekte per Codereflektion zu erreichen.

So oder so, einen Tod muss man sterben^^

Danke jedenfalls, und Abbitte an sarc, den ich wohl nicht komplett/richtig verstanden hatte.

LaTino
Oh, falls jemand drüberstolpert (man weiß ja nie):


public class ObservedZooInhabitant : IZooInhabitant
{
      private int _sleepCount = 0;
      private IZooInhabitant _inhabitant;

      public string Name { get { return _inhabitant.Name; } 
      public ObservedZooInhabitant(IZooInhabitant inhabitant) { _inhabitant = inhabitant; }
      public string Belch() { return _inhabitant.Belch(); }
      public string Fly() { return _inhabitant.Fly(); }
      public string Roar() { return _inhabitant.Roar(); }
      public string Run() { return _inhabitant.Run(); }
      public string Walk() { return _inhabitant.Walk(); }
      public string Swim() { return _inhabitant.Swim(); }
      public string Sleep()
      {
            var sb = new StringBuilder();
            if(++_sleepCount > 3) sb.AppendLine("{0} hat heute schon zu oft geschlafen!!!", _inhabitant.Name);
            sb.AppendLine(_inhabitant.Sleep());
            return sb.ToString();
       }
}

//Aufruf durch die factory:
//statt
var newInhabitant = factory.CreateInhabitant();
//dann
var newInhabitant = new ObservedZooInhabitant(factory.CreateInhabitant());

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)