I have 2 different classes (under same namespace) which implement some same abstract method.
public class SomeClass1 : SomeBaseClass
{
public override ISomeTable Calculate(string s)
{
//some logic
return someTable1;
}
}
public class SomeClass2 : SomeBaseClass
{
public override ISomeTable Calculate(string s)
{
//wait for logic from SomeClass1 to the another logic in SomeClass2
return someTable2;
}
}
Calculate
method is triggered by same source for the two classes.
Can I transfer some logic data (a dict for example) from class 1 to class 2 with waiting for class 1 to finish its calculations?
CodePudding user response:
What you're looking for is the "chain of responsibility" design pattern.
There's quite a few crucial design decisions that are impossible to make given the limited information provided, but need careful consideration:
- Since
ISomeTable
appears to be an interface, how will you decide which implementing class is used for your context data (someTable2)? Does the caller decide that, or your event handlers? It's clearly mutable, but what about polymorphic? Having theCalculate
method's return type beingISomeTable
implies it could return any number of implementers of that interface. And chaining those method calls together could become problematic. - Is the caller responsible for providing the
someTable
instance? Or is it provided by the initial handler? - Should the handlers be aware of who they're passing it on to? Or should there be an event "manager" that handles passing it around to the next one? My example below is an intrusive implementation (self-aware), but that's certainly not the only way of implementation.
- Returning
ISomeTable
from your abstractCalculate
method may not be the optimal choice; especially since we're handing it around as a parameter anyways. - There are no class constructors in this implementation; you may want to store the context data (
someTable
) as a member instance in your base class, and access it through your inheritance instead of passing it on with the method call. This context data needs to be shared per your requirements, but the most efficient (and safe) way of doing so should be decided and tested sooner than later in the development process, since it does affect the API's primary method (Calculate
). - I would probably enforce the factory creation pattern by providing static
Create
methods for your event handlers to ensure initialization is done correctly, and enforce correct usage of the handler classes.
I threw together a very basic implementation based on what I could extrapolate (with a few assumptions) from your example code. This is primarily to point you in the right direction; it's definitely not production-ready, but hopefully will be helpful.
Calling the below program returns the final combined string, built by the chained event handlers from my dummy SomeClass
:
PS ~\so\testapp> dotnet run
"some string (from TableHandler1), some string (from TableHandler2)"
namespace so {
// Our base class all handlers should derive from
public abstract class TableHandler
{
public TableHandler? NextHandler { get; init; }
public abstract ISomeTable Calculate(ISomeTable table, string str);
}
// First Implementation for TableHandler
public class TableHandler1 : TableHandler
{
public override ISomeTable Calculate(ISomeTable table, string str) {
// Ensure we were given a valid instance for storing our logic data
if(table is null)
throw new ArgumentNullException();
// Perform our logic with `str` argument
table.AddString(str " (from TableHandler1)");
// Hand off to next handler, returning the result that propogates back up the chain
if(NextHandler is not null)
table = NextHandler.Calculate(table, str);
return table;
}
}
// Second implementation (less verbose) for TableHandler
public class TableHandler2 : TableHandler
{
public override ISomeTable Calculate(ISomeTable table, string str) {
if(table is null) throw new ArgumentNullException();
table.AddString(str " (from TableHandler2)");
return NextHandler?.Calculate(table, str) ?? table;
}
}
// Dummy api interface for testing
public interface ISomeTable {
public string Data { get; set; }
void AddString(string str);
}
// Dummy implementation for testing
public class SomeTable : ISomeTable {
public string Data { get; set; } = String.Empty;
public void AddString(string str) => $"{Data}, {str}";
}
// Run it
public class Program
{
// Example callsite; note how I'm initializing the chained handlers
public static void Main(string[] args)
{
var handler = new TableHandler1() { NextHandler = new TableHandler2() };
var someTable = handler.Calculate(new SomeTable(), "some string");
Console.WriteLine($"\"{someTable.Data}\"");
}
}
}
CodePudding user response:
This is a forced approach that doesn't scale very well if you have multiple classes which should have the same functionality. Keep in mind that caching some values and modifying the actual baseclass SomeBaseClass can be way more efficient and easier to maintain. However the provided infos you are currently giving are limited. Letting SomeClass2 inherit from SomeClass1 and calling base() before calculation could also be an option.
public class SomeClass1 : SomeBaseClass
{
public override ISomeTable Calculate(string s)
{
//some logic
return someTable1;
}
public ISomeTable Calculate(ISomeTable st)
{
//Calc new table
return newtable;
}
}
public class SomeClass2 : SomeBaseClass
{
public override ISomeTable Calculate(string s)
{
ISomeTable t1=SomeClass1.Calculate(s);
//Modify the t1 according to SomeClass 2 modifications and save results in someTable2
return someTable2;
}
}